From 5d8e9ec8e5287ad3109a68b941b28fea709c839f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 17 Aug 2021 14:41:29 -0400 Subject: [PATCH 01/28] Refactor SpacesContext value's shareToSpacesDataPromise This was originally built for only the "share to spaces" components to consume. However, we now have a need for other consumers too. I renamed it to "spacesDataPromise" and I changed the type for each entry, removing the specific "cannotShareToSpace" flag in favor of a generic "isAuthorizedForPurpose" function that can be used for any purpose. --- .../copy_to_space_flyout_internal.tsx | 2 +- .../components/alias_table.tsx | 9 +++---- .../components/selectable_spaces_control.tsx | 12 ++++----- .../components/share_mode_control.tsx | 4 +-- .../share_to_space_flyout_internal.tsx | 18 ++++++------- .../components/share_to_space_form.tsx | 4 +-- .../public/space_list/space_list_internal.tsx | 17 +++++++------ .../spaces/public/spaces_context/context.tsx | 6 ++--- .../spaces/public/spaces_context/types.ts | 4 +-- .../spaces_context/wrapper_internal.tsx | 25 ++++++++++--------- x-pack/plugins/spaces/public/types.ts | 15 +++++------ 11 files changed, 58 insertions(+), 58 deletions(-) diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx index 8f219b7154def..b20e4ca8c4038 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx @@ -73,7 +73,7 @@ export const CopyToSpaceFlyoutInternal = (props: CopyToSpaceFlyoutProps) => { } ); useEffect(() => { - const getSpaces = spacesManager.getSpaces({ includeAuthorizedPurposes: true }); + const getSpaces = spacesManager.getSpaces({ includeAuthorizedPurposes: true }); // TODO: change to use spacesDataPromise const getActiveSpace = spacesManager.getActiveSpace(); Promise.all([getSpaces, getActiveSpace]) .then(([allSpaces, activeSpace]) => { diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/alias_table.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/alias_table.tsx index 2a2f2470e0199..a8431b1561400 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/alias_table.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/alias_table.tsx @@ -19,7 +19,7 @@ import React, { lazy, Suspense, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { getSpaceAvatarComponent } from '../../space_avatar'; -import type { ShareToSpaceTarget } from '../../types'; +import type { SpacesDataEntry } from '../../types'; import type { InternalLegacyUrlAliasTarget } from './types'; // No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. @@ -28,7 +28,7 @@ const LazySpaceAvatar = lazy(() => ); interface Props { - spaces: ShareToSpaceTarget[]; + spaces: SpacesDataEntry[]; aliasesToDisable: InternalLegacyUrlAliasTarget[]; } @@ -38,10 +38,7 @@ export const AliasTable: FunctionComponent = ({ spaces, aliasesToDisable const spacesMap = useMemo( () => - spaces.reduce( - (acc, space) => acc.set(space.id, space), - new Map() - ), + spaces.reduce((acc, space) => acc.set(space.id, space), new Map()), [spaces] ); const filteredAliasesToDisable = useMemo( diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/selectable_spaces_control.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/selectable_spaces_control.tsx index fad819d35e18a..4e45487a20562 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/selectable_spaces_control.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/selectable_spaces_control.tsx @@ -29,7 +29,7 @@ import { ALL_SPACES_ID, UNKNOWN_SPACE } from '../../../common/constants'; import { DocumentationLinksService } from '../../lib'; import { getSpaceAvatarComponent } from '../../space_avatar'; import { useSpaces } from '../../spaces_context'; -import type { ShareToSpaceTarget } from '../../types'; +import type { SpacesDataEntry } from '../../types'; import type { ShareOptions } from '../types'; import { NoSpacesAvailable } from './no_spaces_available'; @@ -39,7 +39,7 @@ const LazySpaceAvatar = lazy(() => ); interface Props { - spaces: ShareToSpaceTarget[]; + spaces: SpacesDataEntry[]; shareOptions: ShareOptions; onChange: (selectedSpaceIds: string[]) => void; enableCreateNewSpaceLink: boolean; @@ -248,7 +248,7 @@ export const SelectableSpacesControl = (props: Props) => { * Gets additional props for the selection option. */ function getAdditionalProps( - space: ShareToSpaceTarget, + space: SpacesDataEntry, activeSpaceId: string | false, checked: boolean ) { @@ -259,7 +259,7 @@ function getAdditionalProps( checked: 'on' as 'on', }; } - if (space.cannotShareToSpace) { + if (!space.isAuthorizedForPurpose('shareSavedObjectsIntoSpace')) { return { append: ( <> @@ -280,11 +280,11 @@ function getAdditionalProps( } /** - * Given the active space, create a comparator to sort a ShareToSpaceTarget array so that the active space is at the beginning, and space(s) for + * Given the active space, create a comparator to sort a SpacesDataEntry array so that the active space is at the beginning, and space(s) for * which the current feature is disabled are all at the end. */ function createSpacesComparator(activeSpaceId: string | false) { - return (a: ShareToSpaceTarget, b: ShareToSpaceTarget) => { + return (a: SpacesDataEntry, b: SpacesDataEntry) => { if (a.id === activeSpaceId) { return -1; } diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_mode_control.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_mode_control.tsx index 7151f72583d6a..07439542bf839 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_mode_control.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_mode_control.tsx @@ -24,12 +24,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ALL_SPACES_ID } from '../../../common/constants'; import { DocumentationLinksService } from '../../lib'; import { useSpaces } from '../../spaces_context'; -import type { ShareToSpaceTarget } from '../../types'; +import type { SpacesDataEntry } from '../../types'; import type { ShareOptions } from '../types'; import { SelectableSpacesControl } from './selectable_spaces_control'; interface Props { - spaces: ShareToSpaceTarget[]; + spaces: SpacesDataEntry[]; objectNoun: string; canShareToAllSpaces: boolean; shareOptions: ShareOptions; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx index 712adeb26bccb..63dfedc497b94 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx @@ -35,7 +35,7 @@ import { ALL_SPACES_ID, UNKNOWN_SPACE } from '../../../common/constants'; import { getCopyToSpaceFlyoutComponent } from '../../copy_saved_objects_to_space'; import { useSpaces } from '../../spaces_context'; import type { SpacesManager } from '../../spaces_manager'; -import type { ShareToSpaceTarget } from '../../types'; +import type { SpacesDataEntry } from '../../types'; import type { ShareOptions } from '../types'; import { AliasTable } from './alias_table'; import { DEFAULT_OBJECT_NOUN } from './constants'; @@ -124,7 +124,7 @@ function createDefaultChangeSpacesHandler( } export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => { - const { spacesManager, shareToSpacesDataPromise, services } = useSpaces(); + const { spacesManager, spacesDataPromise, services } = useSpaces(); const { notifications } = services; const toastNotifications = notifications!.toasts; @@ -168,7 +168,7 @@ export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => { const [{ isLoading, spaces, referenceGraph, aliasTargets }, setSpacesState] = useState<{ isLoading: boolean; - spaces: ShareToSpaceTarget[]; + spaces: SpacesDataEntry[]; referenceGraph: SavedObjectReferenceWithContext[]; aliasTargets: InternalLegacyUrlAliasTarget[]; }>({ isLoading: true, spaces: [], referenceGraph: [], aliasTargets: [] }); @@ -176,9 +176,9 @@ export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => { const { type, id } = savedObjectTarget; const getShareableReferences = spacesManager.getShareableReferences([{ type, id }]); const getPermissions = spacesManager.getShareSavedObjectPermissions(type); - Promise.all([shareToSpacesDataPromise, getShareableReferences, getPermissions]) - .then(([shareToSpacesData, shareableReferences, permissions]) => { - const activeSpaceId = !enableSpaceAgnosticBehavior && shareToSpacesData.activeSpaceId; + Promise.all([spacesDataPromise, getShareableReferences, getPermissions]) + .then(([spacesData, shareableReferences, permissions]) => { + const activeSpaceId = !enableSpaceAgnosticBehavior && spacesData.activeSpaceId; const selectedSpaceIds = savedObjectTarget.namespaces.filter( (spaceId) => spaceId !== activeSpaceId ); @@ -189,13 +189,13 @@ export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => { setCanShareToAllSpaces(permissions.shareToAllSpaces); setSpacesState({ isLoading: false, - spaces: [...shareToSpacesData.spacesMap].map(([, spaceTarget]) => spaceTarget), + spaces: [...spacesData.spacesMap].map(([, spaceTarget]) => spaceTarget), referenceGraph: shareableReferences.objects, aliasTargets: shareableReferences.objects.reduce( (acc, x) => { for (const space of x.spacesWithMatchingAliases ?? []) { if (space !== '?') { - const spaceExists = shareToSpacesData.spacesMap.has(space); + const spaceExists = spacesData.spacesMap.has(space); // If the user does not have privileges to view all spaces, they will be redacted; we cannot attempt to disable aliases for redacted spaces. acc.push({ targetSpace: space, targetType: x.type, sourceId: x.id, spaceExists }); } @@ -216,7 +216,7 @@ export const ShareToSpaceFlyoutInternal = (props: ShareToSpaceFlyoutProps) => { }, [ savedObjectTarget, spacesManager, - shareToSpacesDataPromise, + spacesDataPromise, toastNotifications, enableSpaceAgnosticBehavior, ]); diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx index 7f8c659805c45..1841d634c6482 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx @@ -12,12 +12,12 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { ShareToSpaceTarget } from '../../types'; +import type { SpacesDataEntry } from '../../types'; import type { ShareOptions } from '../types'; import { ShareModeControl } from './share_mode_control'; interface Props { - spaces: ShareToSpaceTarget[]; + spaces: SpacesDataEntry[]; objectNoun: string; onUpdate: (shareOptions: ShareOptions) => void; shareOptions: ShareOptions; diff --git a/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx b/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx index ac7e6446f2ccd..6c612db9f759d 100644 --- a/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx +++ b/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx @@ -23,7 +23,7 @@ import type { SpaceListProps } from 'src/plugins/spaces_oss/public'; import { ALL_SPACES_ID, UNKNOWN_SPACE } from '../../common/constants'; import { getSpaceAvatarComponent } from '../space_avatar'; import { useSpaces } from '../spaces_context'; -import type { ShareToSpacesData, ShareToSpaceTarget } from '../types'; +import type { SpacesData, SpacesDataEntry } from '../types'; // No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. const LazySpaceAvatar = lazy(() => @@ -31,6 +31,7 @@ const LazySpaceAvatar = lazy(() => ); const DEFAULT_DISPLAY_LIMIT = 5; +type SpaceTarget = Omit; /** * Displays a corresponding list of spaces for a given list of saved object namespaces. It shows up to five spaces (and an indicator for any @@ -43,16 +44,16 @@ export const SpaceListInternal = ({ displayLimit = DEFAULT_DISPLAY_LIMIT, behaviorContext, }: SpaceListProps) => { - const { shareToSpacesDataPromise } = useSpaces(); + const { spacesDataPromise } = useSpaces(); const [isExpanded, setIsExpanded] = useState(false); - const [shareToSpacesData, setShareToSpacesData] = useState(); + const [shareToSpacesData, setShareToSpacesData] = useState(); useEffect(() => { - shareToSpacesDataPromise.then((x) => { + spacesDataPromise.then((x) => { setShareToSpacesData(x); }); - }, [shareToSpacesDataPromise]); + }, [spacesDataPromise]); if (!shareToSpacesData) { return null; @@ -61,7 +62,7 @@ export const SpaceListInternal = ({ const isSharedToAllSpaces = namespaces.includes(ALL_SPACES_ID); const unauthorizedSpacesCount = namespaces.filter((namespace) => namespace === UNKNOWN_SPACE) .length; - let displayedSpaces: ShareToSpaceTarget[]; + let displayedSpaces: SpaceTarget[]; let button: ReactNode = null; if (isSharedToAllSpaces) { @@ -77,8 +78,8 @@ export const SpaceListInternal = ({ ]; } else { const authorized = namespaces.filter((namespace) => namespace !== UNKNOWN_SPACE); - const enabledSpaceTargets: ShareToSpaceTarget[] = []; - const disabledSpaceTargets: ShareToSpaceTarget[] = []; + const enabledSpaceTargets: SpaceTarget[] = []; + const disabledSpaceTargets: SpaceTarget[] = []; authorized.forEach((namespace) => { const spaceTarget = shareToSpacesData.spacesMap.get(namespace); if (spaceTarget === undefined) { diff --git a/x-pack/plugins/spaces/public/spaces_context/context.tsx b/x-pack/plugins/spaces/public/spaces_context/context.tsx index e38a2f17151a9..0b051054e7daf 100644 --- a/x-pack/plugins/spaces/public/spaces_context/context.tsx +++ b/x-pack/plugins/spaces/public/spaces_context/context.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import type { SpacesManager } from '../spaces_manager'; -import type { ShareToSpacesData } from '../types'; +import type { SpacesData } from '../types'; import type { KibanaServices, SpacesReactContext, SpacesReactContextValue } from './types'; const { useContext, createElement, createContext } = React; @@ -25,11 +25,11 @@ export const useSpaces = (): SpacesReactContextValue< export const createSpacesReactContext = ( services: Services, spacesManager: SpacesManager, - shareToSpacesDataPromise: Promise + spacesDataPromise: Promise ): SpacesReactContext => { const value: SpacesReactContextValue = { spacesManager, - shareToSpacesDataPromise, + spacesDataPromise, services, }; const Provider: React.FC = ({ children }) => diff --git a/x-pack/plugins/spaces/public/spaces_context/types.ts b/x-pack/plugins/spaces/public/spaces_context/types.ts index e73da7cb26b68..646bf60ea3012 100644 --- a/x-pack/plugins/spaces/public/spaces_context/types.ts +++ b/x-pack/plugins/spaces/public/spaces_context/types.ts @@ -11,13 +11,13 @@ import type { CoreStart, StartServicesAccessor } from 'src/core/public'; import type { PluginsStart } from '../plugin'; import type { SpacesManager } from '../spaces_manager'; -import type { ShareToSpacesData } from '../types'; +import type { SpacesData } from '../types'; export type KibanaServices = Partial; export interface SpacesReactContextValue { readonly spacesManager: SpacesManager; - readonly shareToSpacesDataPromise: Promise; + readonly spacesDataPromise: Promise; readonly services: Services; } diff --git a/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx b/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx index dd6408e9550ee..a9ab68c864393 100644 --- a/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx +++ b/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx @@ -11,8 +11,9 @@ import React, { useEffect, useMemo, useState } from 'react'; import type { ApplicationStart, DocLinksStart, NotificationsStart } from 'src/core/public'; import type { SpacesContextProps } from 'src/plugins/spaces_oss/public'; +import type { GetAllSpacesPurpose } from '../../common'; import type { SpacesManager } from '../spaces_manager'; -import type { ShareToSpacesData, ShareToSpaceTarget } from '../types'; +import type { SpacesData, SpacesDataEntry } from '../types'; import { createSpacesReactContext } from './context'; import type { InternalProps, SpacesReactContext } from './types'; @@ -22,25 +23,25 @@ interface Services { notifications: NotificationsStart; } -async function getShareToSpacesData( - spacesManager: SpacesManager, - feature?: string -): Promise { +async function getSpacesData(spacesManager: SpacesManager, feature?: string): Promise { const spaces = await spacesManager.getSpaces({ includeAuthorizedPurposes: true }); const activeSpace = await spacesManager.getActiveSpace(); const spacesMap = spaces - .map(({ authorizedPurposes, disabledFeatures, ...space }) => { + .map(({ authorizedPurposes, disabledFeatures, ...space }) => { const isActiveSpace = space.id === activeSpace.id; - const cannotShareToSpace = authorizedPurposes?.shareSavedObjectsIntoSpace === false; const isFeatureDisabled = feature !== undefined && disabledFeatures.includes(feature); return { ...space, ...(isActiveSpace && { isActiveSpace }), - ...(cannotShareToSpace && { cannotShareToSpace }), ...(isFeatureDisabled && { isFeatureDisabled }), + isAuthorizedForPurpose: (purpose: GetAllSpacesPurpose) => + // If authorizedPurposes is not present, then Security is disabled; normally in a situation like this we would "fail-secure", but + // in this case we are dealing with an abstraction over the client-side UI capabilities. There is no chance for privilege + // escalation here, and the correct behavior is that if Security is disabled, the user is implicitly authorized to do everything. + authorizedPurposes ? authorizedPurposes[purpose] === true : true, }; }) - .reduce((acc, cur) => acc.set(cur.id, cur), new Map()); + .reduce((acc, cur) => acc.set(cur.id, cur), new Map()); return { spacesMap, @@ -54,7 +55,7 @@ export const SpacesContextWrapperInternal = ( const { spacesManager, getStartServices, feature, children } = props; const [context, setContext] = useState | undefined>(); - const shareToSpacesDataPromise = useMemo(() => getShareToSpacesData(spacesManager, feature), [ + const spacesDataPromise = useMemo(() => getSpacesData(spacesManager, feature), [ spacesManager, feature, ]); @@ -63,9 +64,9 @@ export const SpacesContextWrapperInternal = ( getStartServices().then(([coreStart]) => { const { application, docLinks, notifications } = coreStart; const services = { application, docLinks, notifications }; - setContext(createSpacesReactContext(services, spacesManager, shareToSpacesDataPromise)); + setContext(createSpacesReactContext(services, spacesManager, spacesDataPromise)); }); - }, [getStartServices, shareToSpacesDataPromise, spacesManager]); + }, [getStartServices, spacesDataPromise, spacesManager]); if (!context) { return null; diff --git a/x-pack/plugins/spaces/public/types.ts b/x-pack/plugins/spaces/public/types.ts index e999e332b9884..3d9eab1c9e441 100644 --- a/x-pack/plugins/spaces/public/types.ts +++ b/x-pack/plugins/spaces/public/types.ts @@ -5,14 +5,14 @@ * 2.0. */ -import type { GetSpaceResult } from '../common'; +import type { GetAllSpacesPurpose, GetSpaceResult } from '../common'; /** * The structure for all of the space data that must be loaded for share-to-space components to function. */ -export interface ShareToSpacesData { - /** A map of each existing space's ID and its associated {@link ShareToSpaceTarget}. */ - readonly spacesMap: Map; +export interface SpacesData { + /** A map of each existing space's ID and its associated {@link SpacesDataEntry}. */ + readonly spacesMap: Map; /** The ID of the active space. */ readonly activeSpaceId: string; } @@ -21,11 +21,12 @@ export interface ShareToSpacesData { * The data that was fetched for a specific space. Includes optional additional fields that are needed to handle edge cases in the * share-to-space components that consume it. */ -export interface ShareToSpaceTarget extends Omit { +export interface SpacesDataEntry + extends Omit { /** True if this space is the active space. */ isActiveSpace?: true; - /** True if the user has read access to this space, but is not authorized to share objects into this space. */ - cannotShareToSpace?: true; /** True if the current feature (specified in the `SpacesContext`) is disabled in this space. */ isFeatureDisabled?: true; + /** Returns true if the user is authorized for the given purpose. */ + isAuthorizedForPurpose(purpose: GetAllSpacesPurpose): boolean; } From 4c50a07705b6fc0b61081efae7b93f1d1e034b23 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 17 Aug 2021 15:26:45 -0400 Subject: [PATCH 02/28] Add CopyToSpaceFlyout to spaces public API contract --- src/plugins/spaces_oss/public/api.mock.ts | 1 + src/plugins/spaces_oss/public/api.ts | 50 +++++++++++++++++++ src/plugins/spaces_oss/public/index.ts | 2 + .../components/copy_to_space_flyout.tsx | 2 +- .../copy_to_space_flyout_internal.test.tsx | 4 +- .../copy_to_space_flyout_internal.tsx | 10 ++-- .../components/copy_to_space_form.tsx | 5 +- .../components/index.ts | 1 - .../components/processing_copy_to_space.tsx | 5 +- .../copy_saved_objects_to_space_action.tsx | 34 +++---------- .../copy_saved_objects_to_space_service.ts | 9 ++-- .../lib/summarize_copy_result.test.ts | 4 +- .../lib/summarize_copy_result.ts | 5 +- .../copy_saved_objects_to_space/types.ts | 27 ---------- x-pack/plugins/spaces/public/plugin.tsx | 2 +- .../spaces/public/ui_api/components.tsx | 2 + x-pack/plugins/spaces/public/ui_api/mocks.ts | 1 + 17 files changed, 85 insertions(+), 79 deletions(-) diff --git a/src/plugins/spaces_oss/public/api.mock.ts b/src/plugins/spaces_oss/public/api.mock.ts index 9ad7599b5ae61..9706ae6b56944 100644 --- a/src/plugins/spaces_oss/public/api.mock.ts +++ b/src/plugins/spaces_oss/public/api.mock.ts @@ -35,6 +35,7 @@ const createApiUiComponentsMock = () => { const mock: SpacesApiUiComponentMock = { getSpacesContextProvider: jest.fn(), getShareToSpaceFlyout: jest.fn(), + getCopyToSpaceFlyout: jest.fn(), getSpaceList: jest.fn(), getLegacyUrlConflict: jest.fn(), getSpaceAvatar: jest.fn(), diff --git a/src/plugins/spaces_oss/public/api.ts b/src/plugins/spaces_oss/public/api.ts index 7492142f0d792..561a1c245fa93 100644 --- a/src/plugins/spaces_oss/public/api.ts +++ b/src/plugins/spaces_oss/public/api.ts @@ -82,6 +82,12 @@ export interface SpacesApiUiComponent { * Note: must be rendered inside of a SpacesContext. */ getShareToSpaceFlyout: LazyComponentFn; + /** + * Displays a flyout to copy an object to other spaces. + * + * Note: must be rendered inside of a SpacesContext. + */ + getCopyToSpaceFlyout: LazyComponentFn; /** * Displays a corresponding list of spaces for a given list of saved object namespaces. It shows up to five spaces (and an indicator for * any number of spaces that the user is not authorized to see) by default. If more than five named spaces would be displayed, the extras @@ -223,6 +229,50 @@ export interface ShareToSpaceSavedObjectTarget { noun?: string; } +/** + * Properties for the CopyToSpaceFlyout. + */ +export interface CopyToSpaceFlyoutProps { + /** + * The object to render the flyout for. + */ + savedObjectTarget: CopyToSpaceSavedObjectTarget; + /** + * Optional callback when the flyout is closed. + */ + onClose?: () => void; +} + +/** + * Describes the target saved object during a copy operation. + */ +export interface CopyToSpaceSavedObjectTarget { + /** + * The object's type. + */ + type: string; + /** + * The object's ID. + */ + id: string; + /** + * The namespaces that the object currently exists in. + */ + namespaces: string[]; + /** + * The EUI icon that is rendered in the flyout's subtitle. + * + * Default is 'apps'. + */ + icon?: string; + /** + * The string that is rendered in the flyout's subtitle. + * + * Default is `${type} [id=${id}]`. + */ + title?: string; +} + /** * Properties for the SpaceList component. */ diff --git a/src/plugins/spaces_oss/public/index.ts b/src/plugins/spaces_oss/public/index.ts index 9c4d5fd17700c..62fc3063fb078 100644 --- a/src/plugins/spaces_oss/public/index.ts +++ b/src/plugins/spaces_oss/public/index.ts @@ -23,6 +23,8 @@ export type { SpacesContextProps, ShareToSpaceFlyoutProps, ShareToSpaceSavedObjectTarget, + CopyToSpaceFlyoutProps, + CopyToSpaceSavedObjectTarget, SpaceListProps, LegacyUrlConflictProps, SpaceAvatarProps, diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx index 3fee2fdfa975d..5d68a5dd14844 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import type { CopyToSpaceFlyoutProps } from './copy_to_space_flyout_internal'; +import type { CopyToSpaceFlyoutProps } from 'src/plugins/spaces_oss/public'; export const getCopyToSpaceFlyoutComponent = async (): Promise< React.FC diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.test.tsx index cb821061b9251..06d4b18311702 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.test.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.test.tsx @@ -13,10 +13,10 @@ import React from 'react'; import { findTestSubject, mountWithIntl, nextTick } from '@kbn/test/jest'; import { coreMock } from 'src/core/public/mocks'; import type { Space } from 'src/plugins/spaces_oss/common'; +import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; import { getSpacesContextProviderWrapper } from '../../spaces_context'; import { spacesManagerMock } from '../../spaces_manager/mocks'; -import type { SavedObjectTarget } from '../types'; import { CopyModeControl } from './copy_mode_control'; import { getCopyToSpaceFlyoutComponent } from './copy_to_space_flyout'; import { CopyToSpaceForm } from './copy_to_space_form'; @@ -82,7 +82,7 @@ const setup = async (opts: SetupOpts = {}) => { namespaces: ['default'], icon: 'dashboard', title: 'foo', - } as SavedObjectTarget; + } as CopyToSpaceSavedObjectTarget; const SpacesContext = await getSpacesContextProviderWrapper({ getStartServices, diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx index b20e4ca8c4038..32b2c24ce45e4 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx @@ -26,19 +26,15 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public'; import type { Space } from 'src/plugins/spaces_oss/common'; +import type { CopyToSpaceFlyoutProps } from 'src/plugins/spaces_oss/public'; import { processImportResponse } from '../../../../../../src/plugins/saved_objects_management/public'; import { useSpaces } from '../../spaces_context'; -import type { CopyOptions, ImportRetry, SavedObjectTarget } from '../types'; +import type { CopyOptions, ImportRetry } from '../types'; import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer'; import { CopyToSpaceForm } from './copy_to_space_form'; import { ProcessingCopyToSpace } from './processing_copy_to_space'; -export interface CopyToSpaceFlyoutProps { - onClose: () => void; - savedObjectTarget: SavedObjectTarget; -} - const INCLUDE_RELATED_DEFAULT = true; const CREATE_NEW_COPIES_DEFAULT = true; const OVERWRITE_ALL_DEFAULT = true; @@ -48,7 +44,7 @@ export const CopyToSpaceFlyoutInternal = (props: CopyToSpaceFlyoutProps) => { const { notifications } = services; const toastNotifications = notifications!.toasts; - const { onClose, savedObjectTarget: object } = props; + const { onClose = () => null, savedObjectTarget: object } = props; const savedObjectTarget = useMemo( () => ({ type: object.type, diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx index e28e95ed42d25..1617d0b6c36f7 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx @@ -10,14 +10,15 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import type { Space } from 'src/plugins/spaces_oss/common'; +import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; -import type { CopyOptions, SavedObjectTarget } from '../types'; +import type { CopyOptions } from '../types'; import type { CopyMode } from './copy_mode_control'; import { CopyModeControl } from './copy_mode_control'; import { SelectableSpacesControl } from './selectable_spaces_control'; interface Props { - savedObjectTarget: Required; + savedObjectTarget: Required; spaces: Space[]; onUpdate: (copyOptions: CopyOptions) => void; copyOptions: CopyOptions; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/index.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/index.ts index f3173e14aa794..6001920f34c11 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/index.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/index.ts @@ -6,4 +6,3 @@ */ export { getCopyToSpaceFlyoutComponent } from './copy_to_space_flyout'; -export type { CopyToSpaceFlyoutProps } from './copy_to_space_flyout_internal'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx index 1fba249e5410a..1ee2ee346de5f 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx @@ -17,13 +17,14 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import type { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public'; import type { Space } from 'src/plugins/spaces_oss/common'; +import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; import { summarizeCopyResult } from '../lib'; -import type { CopyOptions, ImportRetry, SavedObjectTarget } from '../types'; +import type { CopyOptions, ImportRetry } from '../types'; import { SpaceResult, SpaceResultProcessing } from './space_result'; interface Props { - savedObjectTarget: Required; + savedObjectTarget: Required; copyInProgress: boolean; conflictResolutionInProgress: boolean; copyResult: Record; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx index 7818e648dd1cf..da59d529554aa 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx @@ -5,41 +5,23 @@ * 2.0. */ -import React, { lazy } from 'react'; -import useAsync from 'react-use/lib/useAsync'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import type { StartServicesAccessor } from 'src/core/public'; import type { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public'; +import type { CopyToSpaceFlyoutProps, SpacesApiUi } from 'src/plugins/spaces_oss/public'; import { SavedObjectsManagementAction } from '../../../../../src/plugins/saved_objects_management/public'; -import type { PluginsStart } from '../plugin'; -import { SuspenseErrorBoundary } from '../suspense_error_boundary'; -import type { CopyToSpaceFlyoutProps } from './components'; -import { getCopyToSpaceFlyoutComponent } from './components'; - -const LazyCopyToSpaceFlyout = lazy(() => - getCopyToSpaceFlyoutComponent().then((component) => ({ default: component })) -); interface WrapperProps { - getStartServices: StartServicesAccessor; + spacesApiUi: SpacesApiUi; props: CopyToSpaceFlyoutProps; } -const Wrapper = ({ getStartServices, props }: WrapperProps) => { - const { value: startServices = [{ notifications: undefined }] } = useAsync(getStartServices); - const [{ notifications }] = startServices; - - if (!notifications) { - return null; - } +const Wrapper = ({ spacesApiUi, props }: WrapperProps) => { + const LazyComponent = useMemo(() => spacesApiUi.components.getCopyToSpaceFlyout, [spacesApiUi]); - return ( - - - - ); + return ; }; export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagementAction { @@ -62,7 +44,7 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem }, }; - constructor(private getStartServices: StartServicesAccessor) { + constructor(private readonly spacesApiUi: SpacesApiUi) { super(); } @@ -82,7 +64,7 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem }, }; - return ; + return ; }; private onClose = () => { diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts index 17bb26cbf7f11..f3d2c9dd23dde 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts @@ -5,20 +5,19 @@ * 2.0. */ -import type { StartServicesAccessor } from 'src/core/public'; import type { SavedObjectsManagementPluginSetup } from 'src/plugins/saved_objects_management/public'; +import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; -import type { PluginsStart } from '../plugin'; import { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; interface SetupDeps { savedObjectsManagementSetup: SavedObjectsManagementPluginSetup; - getStartServices: StartServicesAccessor; + spacesApiUi: SpacesApiUi; } export class CopySavedObjectsToSpaceService { - public setup({ savedObjectsManagementSetup, getStartServices }: SetupDeps) { - const action = new CopyToSpaceSavedObjectsManagementAction(getStartServices); + public setup({ savedObjectsManagementSetup, spacesApiUi }: SetupDeps) { + const action = new CopyToSpaceSavedObjectsManagementAction(spacesApiUi); savedObjectsManagementSetup.actions.register(action); } } diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts index 6a3d82aaef59c..07f18cda2c7fb 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts @@ -10,8 +10,8 @@ import type { ProcessedImportResponse, SavedObjectsManagementRecord, } from 'src/plugins/saved_objects_management/public'; +import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; -import type { SavedObjectTarget } from '../types'; import { summarizeCopyResult } from './summarize_copy_result'; // Sample data references: @@ -29,7 +29,7 @@ const OBJECTS = { namespaces: [], icon: 'dashboardApp', title: 'my-dashboard-title', - } as Required, + } as Required, MY_DASHBOARD: { type: 'dashboard', id: 'foo', diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts index 68baba12d8b98..07534a47cb860 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts @@ -13,8 +13,7 @@ import type { FailedImport, ProcessedImportResponse, } from 'src/plugins/saved_objects_management/public'; - -import type { SavedObjectTarget } from '../types'; +import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; export interface SummarizedSavedObjectResult { type: string; @@ -68,7 +67,7 @@ export type SummarizedCopyToSpaceResult = | ProcessingResponse; export function summarizeCopyResult( - savedObjectTarget: Required, + savedObjectTarget: Required, copyResult: ProcessedImportResponse | undefined ): SummarizedCopyToSpaceResult { const conflicts = copyResult?.failedImports.filter(isAnyConflict) ?? []; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts index 60a4e176f40bb..48ec1c6552a87 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts @@ -19,30 +19,3 @@ export type ImportRetry = Omit; export interface CopySavedObjectsToSpaceResponse { [spaceId: string]: SavedObjectsImportResponse; } - -export interface SavedObjectTarget { - /** - * The object's type. - */ - type: string; - /** - * The object's ID. - */ - id: string; - /** - * The namespaces that the object currently exists in. - */ - namespaces: string[]; - /** - * The EUI icon that is rendered in the flyout's subtitle. - * - * Default is 'apps'. - */ - icon?: string; - /** - * The string that is rendered in the flyout's subtitle. - * - * Default is `${type} [id=${id}]`. - */ - title?: string; -} diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 1ba1cd1a1f3d4..76977b2425b66 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -93,7 +93,7 @@ export class SpacesPlugin implements Plugin getLegacyUrlConflict({ getStartServices })), getSpaceAvatar: wrapLazy(getSpaceAvatarComponent), diff --git a/x-pack/plugins/spaces/public/ui_api/mocks.ts b/x-pack/plugins/spaces/public/ui_api/mocks.ts index fe3826a58fccc..3e5430c679f2a 100644 --- a/x-pack/plugins/spaces/public/ui_api/mocks.ts +++ b/x-pack/plugins/spaces/public/ui_api/mocks.ts @@ -11,6 +11,7 @@ function createComponentsMock(): jest.Mocked { return { getSpacesContextProvider: jest.fn(), getShareToSpaceFlyout: jest.fn(), + getCopyToSpaceFlyout: jest.fn(), getSpaceList: jest.fn(), getLegacyUrlConflict: jest.fn(), getSpaceAvatar: jest.fn(), From 58301f71a80cc667b21d8e4e88b3702b966e99ad Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 17 Aug 2021 15:44:57 -0400 Subject: [PATCH 03/28] Change CopyToSpaceFlyout to use data from SpacesContext provider Now uses spacesDataPromise instead of fetching all spaces each time the flyout is opened. --- .../copy_status_summary_indicator.tsx | 4 +-- .../copy_to_space_flyout_internal.tsx | 31 +++++++++---------- .../components/copy_to_space_form.tsx | 4 +-- .../components/processing_copy_to_space.tsx | 6 ++-- .../components/selectable_spaces_control.tsx | 4 +-- .../components/space_result.tsx | 5 ++- .../components/space_result_details.tsx | 4 +-- 7 files changed, 28 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx index b04450ae4febd..8d9c2f17bdec6 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx @@ -11,14 +11,14 @@ import { EuiBadge, EuiIconTip, EuiLoadingSpinner } from '@elastic/eui'; import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { SpacesDataEntry } from '../../types'; import type { SummarizedCopyToSpaceResult } from '../lib'; import type { ImportRetry } from '../types'; import { ResolveAllConflicts } from './resolve_all_conflicts'; interface Props { - space: Space; + space: SpacesDataEntry; summarizedCopyResult: SummarizedCopyToSpaceResult; conflictResolutionInProgress: boolean; retries: ImportRetry[]; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx index 32b2c24ce45e4..dc92abacae151 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx @@ -25,11 +25,11 @@ import React, { useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; import type { CopyToSpaceFlyoutProps } from 'src/plugins/spaces_oss/public'; import { processImportResponse } from '../../../../../../src/plugins/saved_objects_management/public'; import { useSpaces } from '../../spaces_context'; +import type { SpacesDataEntry } from '../../types'; import type { CopyOptions, ImportRetry } from '../types'; import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer'; import { CopyToSpaceForm } from './copy_to_space_form'; @@ -40,7 +40,7 @@ const CREATE_NEW_COPIES_DEFAULT = true; const OVERWRITE_ALL_DEFAULT = true; export const CopyToSpaceFlyoutInternal = (props: CopyToSpaceFlyoutProps) => { - const { spacesManager, services } = useSpaces(); + const { spacesManager, spacesDataPromise, services } = useSpaces(); const { notifications } = services; const toastNotifications = notifications!.toasts; @@ -62,22 +62,21 @@ export const CopyToSpaceFlyoutInternal = (props: CopyToSpaceFlyoutProps) => { selectedSpaceIds: [], }); - const [{ isLoading, spaces }, setSpacesState] = useState<{ isLoading: boolean; spaces: Space[] }>( - { - isLoading: true, - spaces: [], - } - ); + const [{ isLoading, spaces }, setSpacesState] = useState<{ + isLoading: boolean; + spaces: SpacesDataEntry[]; + }>({ + isLoading: true, + spaces: [], + }); useEffect(() => { - const getSpaces = spacesManager.getSpaces({ includeAuthorizedPurposes: true }); // TODO: change to use spacesDataPromise - const getActiveSpace = spacesManager.getActiveSpace(); - Promise.all([getSpaces, getActiveSpace]) - .then(([allSpaces, activeSpace]) => { + spacesDataPromise + .then(({ spacesMap }) => { setSpacesState({ isLoading: false, - spaces: allSpaces.filter( - ({ id, authorizedPurposes }) => - id !== activeSpace.id && authorizedPurposes?.copySavedObjectsIntoSpace !== false + spaces: [...spacesMap.values()].filter( + ({ isActiveSpace, isAuthorizedForPurpose }) => + isActiveSpace || isAuthorizedForPurpose('copySavedObjectsIntoSpace') ), }); }) @@ -88,7 +87,7 @@ export const CopyToSpaceFlyoutInternal = (props: CopyToSpaceFlyoutProps) => { }), }); }); - }, [spacesManager, toastNotifications]); + }, [spacesDataPromise, toastNotifications]); const [copyInProgress, setCopyInProgress] = useState(false); const [conflictResolutionInProgress, setConflictResolutionInProgress] = useState(false); diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx index 1617d0b6c36f7..e095becf68fbe 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx @@ -9,9 +9,9 @@ import { EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Space } from 'src/plugins/spaces_oss/common'; import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; +import type { SpacesDataEntry } from '../../types'; import type { CopyOptions } from '../types'; import type { CopyMode } from './copy_mode_control'; import { CopyModeControl } from './copy_mode_control'; @@ -19,7 +19,7 @@ import { SelectableSpacesControl } from './selectable_spaces_control'; interface Props { savedObjectTarget: Required; - spaces: Space[]; + spaces: SpacesDataEntry[]; onUpdate: (copyOptions: CopyOptions) => void; copyOptions: CopyOptions; } diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx index 1ee2ee346de5f..fe15ae16b252a 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx @@ -16,9 +16,9 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import type { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; +import type { SpacesDataEntry } from '../../types'; import { summarizeCopyResult } from '../lib'; import type { CopyOptions, ImportRetry } from '../types'; import { SpaceResult, SpaceResultProcessing } from './space_result'; @@ -30,7 +30,7 @@ interface Props { copyResult: Record; retries: Record; onRetriesChange: (retries: Record) => void; - spaces: Space[]; + spaces: SpacesDataEntry[]; copyOptions: CopyOptions; } @@ -96,7 +96,7 @@ export const ProcessingCopyToSpace = (props: Props) => { {props.copyOptions.selectedSpaceIds.map((id) => { - const space = props.spaces.find((s) => s.id === id) as Space; + const space = props.spaces.find((s) => s.id === id)!; const spaceCopyResult = props.copyResult[space.id]; const summarizedSpaceCopyResult = summarizeCopyResult( props.savedObjectTarget, diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx index 2f96646844a35..57b55ac5a2618 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx @@ -12,10 +12,10 @@ import { EuiIconTip, EuiLoadingSpinner, EuiSelectable } from '@elastic/eui'; import React, { lazy, Suspense } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Space } from 'src/plugins/spaces_oss/common'; import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; +import type { SpacesDataEntry } from '../../types'; // No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. const LazySpaceAvatar = lazy(() => @@ -23,7 +23,7 @@ const LazySpaceAvatar = lazy(() => ); interface Props { - spaces: Space[]; + spaces: SpacesDataEntry[]; selectedSpaceIds: string[]; disabledSpaceIds: Set; onChange: (selectedSpaceIds: string[]) => void; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx index 6d14584ac21a9..932b8bc9254f6 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result.tsx @@ -17,9 +17,8 @@ import { } from '@elastic/eui'; import React, { lazy, Suspense, useState } from 'react'; -import type { Space } from 'src/plugins/spaces_oss/common'; - import { getSpaceAvatarComponent } from '../../space_avatar'; +import type { SpacesDataEntry } from '../../types'; import type { SummarizedCopyToSpaceResult } from '../lib'; import type { ImportRetry } from '../types'; import { CopyStatusSummaryIndicator } from './copy_status_summary_indicator'; @@ -31,7 +30,7 @@ const LazySpaceAvatar = lazy(() => ); interface Props { - space: Space; + space: SpacesDataEntry; summarizedCopyResult: SummarizedCopyToSpaceResult; retries: ImportRetry[]; onRetriesChange: (retries: ImportRetry[]) => void; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx index e2db8e7fb7480..d58490a86b7f5 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/space_result_details.tsx @@ -25,15 +25,15 @@ import type { SavedObjectsImportAmbiguousConflictError, SavedObjectsImportConflictError, } from 'src/core/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { SpacesDataEntry } from '../../types'; import type { SummarizedCopyToSpaceResult } from '../lib'; import type { ImportRetry } from '../types'; import { CopyStatusIndicator } from './copy_status_indicator'; interface Props { summarizedCopyResult: SummarizedCopyToSpaceResult; - space: Space; + space: SpacesDataEntry; retries: ImportRetry[]; onRetriesChange: (retries: ImportRetry[]) => void; destinationMap: Map; From c420dbe6686f12bc708c5e08dd52b752257d2a8f Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 17 Aug 2021 18:02:40 -0400 Subject: [PATCH 04/28] Remove dependencies on spacesOss plugin The dashboard and savedObjectsManagement plugins now both depend on the spaces plugin directly! --- src/plugins/dashboard/kibana.json | 2 +- .../public/application/dashboard_router.tsx | 2 +- src/plugins/dashboard/public/plugin.tsx | 4 +- .../dashboard/public/services/spaces.ts | 2 +- src/plugins/dashboard/tsconfig.json | 2 +- .../saved_objects_management/kibana.json | 2 +- .../management_section/mount_section.tsx | 4 +- .../saved_objects_table_page.tsx | 7 +--- .../saved_objects_management/public/plugin.ts | 10 ++--- .../public/services/action_service.test.ts | 13 ++++++- .../public/services/action_service.ts | 18 ++++++++- .../copy_saved_objects_to_space_action.tsx | 18 +++++---- .../public/services/actions/index.ts | 10 +++++ ...are_saved_objects_to_space_action.test.tsx | 10 ++--- .../share_saved_objects_to_space_action.tsx | 18 +++++---- .../public/services/column_service.test.ts | 9 ++++- .../public/services/column_service.ts | 15 +++++++- .../public/services/columns/index.ts | 9 +++++ .../share_saved_objects_to_space_column.tsx | 14 ++++--- .../saved_objects_management/tsconfig.json | 2 +- x-pack/plugins/spaces/kibana.json | 3 +- ...opy_saved_objects_to_space_service.test.ts | 32 ---------------- .../copy_saved_objects_to_space_service.ts | 23 ----------- .../copy_saved_objects_to_space/index.ts | 1 - x-pack/plugins/spaces/public/index.ts | 8 ++++ x-pack/plugins/spaces/public/plugin.tsx | 17 --------- .../share_saved_objects_to_space/index.ts | 1 - ...are_saved_objects_to_space_service.test.ts | 38 ------------------- .../share_saved_objects_to_space_service.ts | 27 ------------- .../translations/translations/ja-JP.json | 6 --- .../translations/translations/zh-CN.json | 6 --- 31 files changed, 128 insertions(+), 205 deletions(-) rename {x-pack/plugins/spaces/public/copy_saved_objects_to_space => src/plugins/saved_objects_management/public/services/actions}/copy_saved_objects_to_space_action.tsx (75%) create mode 100644 src/plugins/saved_objects_management/public/services/actions/index.ts rename {x-pack/plugins/spaces/public/share_saved_objects_to_space => src/plugins/saved_objects_management/public/services/actions}/share_saved_objects_to_space_action.test.tsx (88%) rename {x-pack/plugins/spaces/public/share_saved_objects_to_space => src/plugins/saved_objects_management/public/services/actions}/share_saved_objects_to_space_action.tsx (79%) create mode 100644 src/plugins/saved_objects_management/public/services/columns/index.ts rename {x-pack/plugins/spaces/public/share_saved_objects_to_space => src/plugins/saved_objects_management/public/services/columns}/share_saved_objects_to_space_column.tsx (69%) delete mode 100644 x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts delete mode 100644 x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts delete mode 100644 x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.test.ts delete mode 100644 x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.ts diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index d270b7dad3c7c..164be971d22b7 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -19,7 +19,7 @@ "presentationUtil", "visualizations" ], - "optionalPlugins": ["home", "spacesOss", "savedObjectsTaggingOss", "usageCollection"], + "optionalPlugins": ["home", "spaces", "savedObjectsTaggingOss", "usageCollection"], "server": true, "ui": true, "requiredBundles": ["home", "kibanaReact", "kibanaUtils", "presentationUtil"] diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index cb40b30542869..7c538c0f3bf97 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -79,6 +79,7 @@ export async function mountApp({ urlForwarding, data: dataStart, share: shareStart, + spaces: spacesApi, embeddable: embeddableStart, kibanaLegacy: { dashboardConfig }, savedObjectsTaggingOss, @@ -86,7 +87,6 @@ export async function mountApp({ presentationUtil, } = pluginsStart; - const spacesApi = pluginsStart.spacesOss?.isSpacesAvailable ? pluginsStart.spacesOss : undefined; const activeSpaceId = spacesApi && (await spacesApi.getActiveSpace$().pipe(first()).toPromise())?.id; let globalEmbedSettings: DashboardEmbedSettings | undefined; diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 3da299bb20095..1973fd4dbe394 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -80,7 +80,7 @@ import { UrlGeneratorState } from '../../share/public'; import { ExportCSVAction } from './application/actions/export_csv_action'; import { dashboardFeatureCatalog } from './dashboard_strings'; import { replaceUrlHashQuery } from '../../kibana_utils/public'; -import { SpacesOssPluginStart } from './services/spaces'; +import { SpacesPluginStart } from './services/spaces'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -118,7 +118,7 @@ export interface DashboardStartDependencies { savedObjects: SavedObjectsStart; presentationUtil: PresentationUtilPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; - spacesOss?: SpacesOssPluginStart; + spaces?: SpacesPluginStart; visualizations: VisualizationsStart; } diff --git a/src/plugins/dashboard/public/services/spaces.ts b/src/plugins/dashboard/public/services/spaces.ts index e6d2c6400818f..89a0acaf611bd 100644 --- a/src/plugins/dashboard/public/services/spaces.ts +++ b/src/plugins/dashboard/public/services/spaces.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { SpacesOssPluginStart } from '../../../spaces_oss/public'; +export { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public'; diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 4febb8b5555cf..5b77b215a9f69 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -14,6 +14,7 @@ "server/**/*", ], "references": [ + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, { "path": "../../core/tsconfig.json" }, { "path": "../inspector/tsconfig.json" }, { "path": "../kibana_legacy/tsconfig.json" }, @@ -30,7 +31,6 @@ { "path": "../saved_objects_tagging_oss/tsconfig.json" }, { "path": "../saved_objects/tsconfig.json" }, { "path": "../ui_actions/tsconfig.json" }, - { "path": "../spaces_oss/tsconfig.json" }, { "path": "../charts/tsconfig.json" }, { "path": "../discover/tsconfig.json" }, { "path": "../visualizations/tsconfig.json" }, diff --git a/src/plugins/saved_objects_management/kibana.json b/src/plugins/saved_objects_management/kibana.json index 48e61eb9e4da5..b8207e0627b81 100644 --- a/src/plugins/saved_objects_management/kibana.json +++ b/src/plugins/saved_objects_management/kibana.json @@ -8,7 +8,7 @@ "server": true, "ui": true, "requiredPlugins": ["management", "data"], - "optionalPlugins": ["dashboard", "visualizations", "discover", "home", "savedObjectsTaggingOss", "spacesOss"], + "optionalPlugins": ["dashboard", "visualizations", "discover", "home", "savedObjectsTaggingOss", "spaces"], "extraPublicDirs": ["public/lib"], "requiredBundles": ["kibanaReact", "home"] } diff --git a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx index a21ad6b7a440a..e5aaec6fa4bbc 100644 --- a/src/plugins/saved_objects_management/public/management_section/mount_section.tsx +++ b/src/plugins/saved_objects_management/public/management_section/mount_section.tsx @@ -39,7 +39,7 @@ export const mountManagementSection = async ({ }: MountParams) => { const [ coreStart, - { data, savedObjectsTaggingOss, spacesOss }, + { data, savedObjectsTaggingOss, spaces: spacesApi }, pluginStart, ] = await core.getStartServices(); const { element, history, setBreadcrumbs } = mountParams; @@ -61,8 +61,6 @@ export const mountManagementSection = async ({ return children! as React.ReactElement; }; - const spacesApi = spacesOss?.isSpacesAvailable ? spacesOss : undefined; - ReactDOM.render( diff --git a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx index fd938abd2704b..f22f0333ec229 100644 --- a/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx +++ b/src/plugins/saved_objects_management/public/management_section/saved_objects_table_page.tsx @@ -13,10 +13,7 @@ import { Query } from '@elastic/eui'; import { parse } from 'query-string'; import { i18n } from '@kbn/i18n'; import { CoreStart, ChromeBreadcrumb } from 'src/core/public'; -import type { - SpacesAvailableStartContract, - SpacesContextProps, -} from 'src/plugins/spaces_oss/public'; +import type { SpacesApi, SpacesContextProps } from '../../../../../x-pack/plugins/spaces/public'; import { DataPublicPluginStart } from '../../../data/public'; import { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; import { @@ -42,7 +39,7 @@ const SavedObjectsTablePage = ({ coreStart: CoreStart; dataStart: DataPublicPluginStart; taggingApi?: SavedObjectsTaggingApi; - spacesApi?: SpacesAvailableStartContract; + spacesApi?: SpacesApi; allowedTypes: string[]; serviceRegistry: ISavedObjectsManagementServiceRegistry; actionRegistry: SavedObjectsManagementActionServiceStart; diff --git a/src/plugins/saved_objects_management/public/plugin.ts b/src/plugins/saved_objects_management/public/plugin.ts index f4578c4c4b8e1..cc6bd83005463 100644 --- a/src/plugins/saved_objects_management/public/plugin.ts +++ b/src/plugins/saved_objects_management/public/plugin.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; import { ManagementSetup } from '../../management/public'; import { DataPublicPluginStart } from '../../data/public'; import { DashboardStart } from '../../dashboard/public'; @@ -15,7 +16,6 @@ import { DiscoverStart } from '../../discover/public'; import { HomePublicPluginSetup, FeatureCatalogueCategory } from '../../home/public'; import { VisualizationsStart } from '../../visualizations/public'; import { SavedObjectTaggingOssPluginStart } from '../../saved_objects_tagging_oss/public'; -import type { SpacesOssPluginStart } from '../../spaces_oss/public'; import { SavedObjectsManagementActionService, SavedObjectsManagementActionServiceSetup, @@ -50,7 +50,7 @@ export interface StartDependencies { visualizations?: VisualizationsStart; discover?: DiscoverStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; - spacesOss?: SpacesOssPluginStart; + spaces?: SpacesPluginStart; } export class SavedObjectsManagementPlugin @@ -116,9 +116,9 @@ export class SavedObjectsManagementPlugin }; } - public start(core: CoreStart, { data }: StartDependencies) { - const actionStart = this.actionService.start(); - const columnStart = this.columnService.start(); + public start(_core: CoreStart, { spaces: spacesApi }: StartDependencies) { + const actionStart = this.actionService.start(spacesApi); + const columnStart = this.columnService.start(spacesApi); return { actions: actionStart, diff --git a/src/plugins/saved_objects_management/public/services/action_service.test.ts b/src/plugins/saved_objects_management/public/services/action_service.test.ts index 609cd5e5d3a04..a86c27a13e6f2 100644 --- a/src/plugins/saved_objects_management/public/services/action_service.test.ts +++ b/src/plugins/saved_objects_management/public/services/action_service.test.ts @@ -6,6 +6,11 @@ * Side Public License, v 1. */ +import { spacesApiMock } from '../../../spaces_oss/public/api.mock'; +import { + CopyToSpaceSavedObjectsManagementAction, + ShareToSpaceSavedObjectsManagementAction, +} from './actions'; import { SavedObjectsManagementActionService, SavedObjectsManagementActionServiceSetup, @@ -44,8 +49,12 @@ describe('SavedObjectsManagementActionRegistry', () => { it('allows actions to be registered and retrieved', () => { const action = createAction('foo'); setup.register(action); - const start = service.start(); - expect(start.getAll()).toContain(action); + const start = service.start(spacesApiMock.create()); + expect(start.getAll()).toEqual([ + action, + expect.any(ShareToSpaceSavedObjectsManagementAction), + expect.any(CopyToSpaceSavedObjectsManagementAction), + ]); }); it('does not allow actions with duplicate ids to be registered', () => { diff --git a/src/plugins/saved_objects_management/public/services/action_service.ts b/src/plugins/saved_objects_management/public/services/action_service.ts index 015a4953fe238..b72ca3d2535de 100644 --- a/src/plugins/saved_objects_management/public/services/action_service.ts +++ b/src/plugins/saved_objects_management/public/services/action_service.ts @@ -6,6 +6,11 @@ * Side Public License, v 1. */ +import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; +import { + CopyToSpaceSavedObjectsManagementAction, + ShareToSpaceSavedObjectsManagementAction, +} from './actions'; import { SavedObjectsManagementAction } from './types'; export interface SavedObjectsManagementActionServiceSetup { @@ -40,10 +45,21 @@ export class SavedObjectsManagementActionService { }; } - start(): SavedObjectsManagementActionServiceStart { + start(spacesApi?: SpacesApi): SavedObjectsManagementActionServiceStart { + if (spacesApi) { + registerSpacesApiActions(this, spacesApi); + } return { has: (actionId) => this.actions.has(actionId), getAll: () => [...this.actions.values()], }; } } + +function registerSpacesApiActions( + service: SavedObjectsManagementActionService, + spacesApi: SpacesApi +) { + service.setup().register(new ShareToSpaceSavedObjectsManagementAction(spacesApi.ui)); + service.setup().register(new CopyToSpaceSavedObjectsManagementAction(spacesApi.ui)); +} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx b/src/plugins/saved_objects_management/public/services/actions/copy_saved_objects_to_space_action.tsx similarity index 75% rename from x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx rename to src/plugins/saved_objects_management/public/services/actions/copy_saved_objects_to_space_action.tsx index da59d529554aa..5773f64a1e628 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_action.tsx +++ b/src/plugins/saved_objects_management/public/services/actions/copy_saved_objects_to_space_action.tsx @@ -1,17 +1,21 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import type { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public'; -import type { CopyToSpaceFlyoutProps, SpacesApiUi } from 'src/plugins/spaces_oss/public'; -import { SavedObjectsManagementAction } from '../../../../../src/plugins/saved_objects_management/public'; +import type { + CopyToSpaceFlyoutProps, + SpacesApiUi, +} from '../../../../../../x-pack/plugins/spaces/public'; +import type { SavedObjectsManagementRecord } from '../types'; +import { SavedObjectsManagementAction } from '../types'; interface WrapperProps { spacesApiUi: SpacesApiUi; @@ -28,10 +32,10 @@ export class CopyToSpaceSavedObjectsManagementAction extends SavedObjectsManagem public id: string = 'copy_saved_objects_to_space'; public euiAction = { - name: i18n.translate('xpack.spaces.management.copyToSpace.actionTitle', { + name: i18n.translate('savedObjectsManagement.copyToSpace.actionTitle', { defaultMessage: 'Copy to space', }), - description: i18n.translate('xpack.spaces.management.copyToSpace.actionDescription', { + description: i18n.translate('savedObjectsManagement.copyToSpace.actionDescription', { defaultMessage: 'Make a copy of this saved object in one or more spaces', }), icon: 'copy', diff --git a/src/plugins/saved_objects_management/public/services/actions/index.ts b/src/plugins/saved_objects_management/public/services/actions/index.ts new file mode 100644 index 0000000000000..39cde652fd54f --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/actions/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; +export { ShareToSpaceSavedObjectsManagementAction } from './share_saved_objects_to_space_action'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.test.tsx b/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.test.tsx similarity index 88% rename from x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.test.tsx rename to src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.test.tsx index 9c3a56aac20ad..61a4627e030f9 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.test.tsx +++ b/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.test.tsx @@ -1,13 +1,13 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ -import type { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public'; - -import { uiApiMock } from '../ui_api/mocks'; +import { uiApiMock } from '../../../../../../x-pack/plugins/spaces/public/ui_api/mocks'; +import type { SavedObjectsManagementRecord } from '../types'; import { ShareToSpaceSavedObjectsManagementAction } from './share_saved_objects_to_space_action'; describe('ShareToSpaceSavedObjectsManagementAction', () => { diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx b/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.tsx similarity index 79% rename from x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx rename to src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.tsx index 90dda8ad0b013..e36c13bd8fd8b 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_action.tsx +++ b/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.tsx @@ -1,17 +1,21 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import type { SavedObjectsManagementRecord } from 'src/plugins/saved_objects_management/public'; -import type { ShareToSpaceFlyoutProps, SpacesApiUi } from 'src/plugins/spaces_oss/public'; -import { SavedObjectsManagementAction } from '../../../../../src/plugins/saved_objects_management/public'; +import type { + ShareToSpaceFlyoutProps, + SpacesApiUi, +} from '../../../../../../x-pack/plugins/spaces/public'; +import type { SavedObjectsManagementRecord } from '../types'; +import { SavedObjectsManagementAction } from '../types'; interface WrapperProps { spacesApiUi: SpacesApiUi; @@ -28,10 +32,10 @@ export class ShareToSpaceSavedObjectsManagementAction extends SavedObjectsManage public id: string = 'share_saved_objects_to_space'; public euiAction = { - name: i18n.translate('xpack.spaces.shareToSpace.actionTitle', { + name: i18n.translate('savedObjectsManagement.shareToSpace.actionTitle', { defaultMessage: 'Share to space', }), - description: i18n.translate('xpack.spaces.shareToSpace.actionDescription', { + description: i18n.translate('savedObjectsManagement.shareToSpace.actionDescription', { defaultMessage: 'Share this saved object to one or more spaces', }), icon: 'share', diff --git a/src/plugins/saved_objects_management/public/services/column_service.test.ts b/src/plugins/saved_objects_management/public/services/column_service.test.ts index 3e18cdaec0c47..fcd3b6c7cd717 100644 --- a/src/plugins/saved_objects_management/public/services/column_service.test.ts +++ b/src/plugins/saved_objects_management/public/services/column_service.test.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import { spacesApiMock } from '../../../spaces_oss/public/api.mock'; +// import { ShareToSpaceSavedObjectsManagementColumn } from './columns'; import { SavedObjectsManagementColumnService, SavedObjectsManagementColumnServiceSetup, @@ -40,8 +42,11 @@ describe('SavedObjectsManagementColumnRegistry', () => { it('allows columns to be registered and retrieved', () => { const column = createColumn('foo'); setup.register(column); - const start = service.start(); - expect(start.getAll()).toContain(column); + const start = service.start(spacesApiMock.create()); + expect(start.getAll()).toEqual([ + column, + // expect.any(ShareToSpaceSavedObjectsManagementColumn), + ]); }); it('does not allow columns with duplicate ids to be registered', () => { diff --git a/src/plugins/saved_objects_management/public/services/column_service.ts b/src/plugins/saved_objects_management/public/services/column_service.ts index fb919af2b4028..74c06a3d33218 100644 --- a/src/plugins/saved_objects_management/public/services/column_service.ts +++ b/src/plugins/saved_objects_management/public/services/column_service.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import type { SpacesApi } from '../../../../../x-pack/plugins/spaces/public'; +// import { ShareToSpaceSavedObjectsManagementColumn } from './columns'; import { SavedObjectsManagementColumn } from './types'; export interface SavedObjectsManagementColumnServiceSetup { @@ -36,9 +38,20 @@ export class SavedObjectsManagementColumnService { }; } - start(): SavedObjectsManagementColumnServiceStart { + start(spacesApi?: SpacesApi): SavedObjectsManagementColumnServiceStart { + if (spacesApi) { + registerSpacesApiColumns(this, spacesApi); + } return { getAll: () => [...this.columns.values()], }; } } + +function registerSpacesApiColumns( + service: SavedObjectsManagementColumnService, + spacesApi: SpacesApi +) { + // Note: this column is hidden for now because no saved objects are shareable. It should be uncommented when at least one saved object type is multi-namespace. + // service.setup().register(new ShareToSpaceSavedObjectsManagementColumn(spacesApi.ui)); +} diff --git a/src/plugins/saved_objects_management/public/services/columns/index.ts b/src/plugins/saved_objects_management/public/services/columns/index.ts new file mode 100644 index 0000000000000..f93c603f9011d --- /dev/null +++ b/src/plugins/saved_objects_management/public/services/columns/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { ShareToSpaceSavedObjectsManagementColumn } from './share_saved_objects_to_space_column'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx b/src/plugins/saved_objects_management/public/services/columns/share_saved_objects_to_space_column.tsx similarity index 69% rename from x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx rename to src/plugins/saved_objects_management/public/services/columns/share_saved_objects_to_space_column.tsx index 609811cd6b7ce..736b656f15d93 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx +++ b/src/plugins/saved_objects_management/public/services/columns/share_saved_objects_to_space_column.tsx @@ -1,15 +1,17 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. */ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import type { SavedObjectsManagementColumn } from 'src/plugins/saved_objects_management/public'; -import type { SpaceListProps, SpacesApiUi } from 'src/plugins/spaces_oss/public'; + +import type { SpaceListProps, SpacesApiUi } from '../../../../../../x-pack/plugins/spaces/public'; +import type { SavedObjectsManagementColumn } from '../types'; interface WrapperProps { spacesApiUi: SpacesApiUi; @@ -28,10 +30,10 @@ export class ShareToSpaceSavedObjectsManagementColumn public euiColumn = { field: 'namespaces', - name: i18n.translate('xpack.spaces.shareToSpace.columnTitle', { + name: i18n.translate('savedObjectsManagement.shareToSpace.columnTitle', { defaultMessage: 'Shared spaces', }), - description: i18n.translate('xpack.spaces.shareToSpace.columnDescription', { + description: i18n.translate('savedObjectsManagement.shareToSpace.columnDescription', { defaultMessage: 'The other spaces that this object is currently shared to', }), render: (namespaces: string[] | undefined) => { diff --git a/src/plugins/saved_objects_management/tsconfig.json b/src/plugins/saved_objects_management/tsconfig.json index 0f26da69acd17..f8c11db8361c2 100644 --- a/src/plugins/saved_objects_management/tsconfig.json +++ b/src/plugins/saved_objects_management/tsconfig.json @@ -13,6 +13,7 @@ ], "references": [ { "path": "../../core/tsconfig.json" }, + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, { "path": "../dashboard/tsconfig.json" }, { "path": "../data/tsconfig.json" }, { "path": "../discover/tsconfig.json" }, @@ -20,6 +21,5 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../management/tsconfig.json" }, { "path": "../visualizations/tsconfig.json" }, - { "path": "../spaces_oss/tsconfig.json" }, ] } diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index e01224d03bfc3..68ea4c18cfacc 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -13,8 +13,7 @@ "advancedSettings", "home", "management", - "usageCollection", - "savedObjectsManagement" + "usageCollection" ], "server": true, "ui": true, diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts deleted file mode 100644 index f55a7d8054608..0000000000000 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { coreMock } from 'src/core/public/mocks'; -import { savedObjectsManagementPluginMock } from 'src/plugins/saved_objects_management/public/mocks'; - -import { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; -import { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space_service'; - -describe('CopySavedObjectsToSpaceService', () => { - describe('#setup', () => { - it('registers the CopyToSpaceSavedObjectsManagementAction', () => { - const { getStartServices } = coreMock.createSetup(); - const deps = { - savedObjectsManagementSetup: savedObjectsManagementPluginMock.createSetupContract(), - getStartServices, - }; - - const service = new CopySavedObjectsToSpaceService(); - service.setup(deps); - - expect(deps.savedObjectsManagementSetup.actions.register).toHaveBeenCalledTimes(1); - expect(deps.savedObjectsManagementSetup.actions.register).toHaveBeenCalledWith( - expect.any(CopyToSpaceSavedObjectsManagementAction) - ); - }); - }); -}); diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts deleted file mode 100644 index f3d2c9dd23dde..0000000000000 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/copy_saved_objects_to_space_service.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { SavedObjectsManagementPluginSetup } from 'src/plugins/saved_objects_management/public'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; - -import { CopyToSpaceSavedObjectsManagementAction } from './copy_saved_objects_to_space_action'; - -interface SetupDeps { - savedObjectsManagementSetup: SavedObjectsManagementPluginSetup; - spacesApiUi: SpacesApiUi; -} - -export class CopySavedObjectsToSpaceService { - public setup({ savedObjectsManagementSetup, spacesApiUi }: SetupDeps) { - const action = new CopyToSpaceSavedObjectsManagementAction(spacesApiUi); - savedObjectsManagementSetup.actions.register(action); - } -} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/index.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/index.ts index abbbc8ba1368f..bbaa6a49f7a47 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/index.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/index.ts @@ -6,4 +6,3 @@ */ export { getCopyToSpaceFlyoutComponent } from './components'; -export { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space_service'; diff --git a/x-pack/plugins/spaces/public/index.ts b/x-pack/plugins/spaces/public/index.ts index 08dda35a6eb10..1f3c2ba5b4c84 100644 --- a/x-pack/plugins/spaces/public/index.ts +++ b/x-pack/plugins/spaces/public/index.ts @@ -15,6 +15,14 @@ export type { GetAllSpacesPurpose, GetSpaceResult } from '../common'; // re-export types from oss definition export type { Space } from 'src/plugins/spaces_oss/common'; +export type { + SpacesApi, + SpacesApiUi, + SpacesContextProps, + ShareToSpaceFlyoutProps, + CopyToSpaceFlyoutProps, + SpaceListProps, +} from 'src/plugins/spaces_oss/public'; export const plugin = () => { return new SpacesPlugin(); diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 76977b2425b66..56e2f1143ae55 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -9,16 +9,13 @@ import type { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import type { AdvancedSettingsSetup } from 'src/plugins/advanced_settings/public'; import type { HomePublicPluginSetup } from 'src/plugins/home/public'; import type { ManagementSetup, ManagementStart } from 'src/plugins/management/public'; -import type { SavedObjectsManagementPluginSetup } from 'src/plugins/saved_objects_management/public'; import type { SpacesApi, SpacesOssPluginSetup } from 'src/plugins/spaces_oss/public'; import type { FeaturesPluginStart } from '../../features/public'; import { AdvancedSettingsService } from './advanced_settings'; -import { CopySavedObjectsToSpaceService } from './copy_saved_objects_to_space'; import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry'; import { ManagementService } from './management'; import { initSpacesNavControl } from './nav_control'; -import { ShareSavedObjectsToSpaceService } from './share_saved_objects_to_space'; import { spaceSelectorApp } from './space_selector'; import { SpacesManager } from './spaces_manager'; import { getUiApi } from './ui_api'; @@ -28,7 +25,6 @@ export interface PluginsSetup { advancedSettings?: AdvancedSettingsSetup; home?: HomePublicPluginSetup; management?: ManagementSetup; - savedObjectsManagement?: SavedObjectsManagementPluginSetup; } export interface PluginsStart { @@ -84,19 +80,6 @@ export class SpacesPlugin implements Plugin { - describe('#setup', () => { - it('registers the ShareToSpaceSavedObjectsManagement Action and Column', () => { - const deps = { - savedObjectsManagementSetup: savedObjectsManagementPluginMock.createSetupContract(), - spacesApiUi: uiApiMock.create(), - }; - - const service = new ShareSavedObjectsToSpaceService(); - service.setup(deps); - - expect(deps.savedObjectsManagementSetup.actions.register).toHaveBeenCalledTimes(1); - expect(deps.savedObjectsManagementSetup.actions.register).toHaveBeenCalledWith( - expect.any(ShareToSpaceSavedObjectsManagementAction) - ); - - // expect(deps.savedObjectsManagementSetup.columns.register).toHaveBeenCalledTimes(1); - // expect(deps.savedObjectsManagementSetup.columns.register).toHaveBeenCalledWith( - // expect.any(ShareToSpaceSavedObjectsManagementColumn) - // ); - expect(deps.savedObjectsManagementSetup.columns.register).not.toHaveBeenCalled(); // ensure this test fails after column code is uncommented - }); - }); -}); diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.ts deleted file mode 100644 index bc70347760465..0000000000000 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_service.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { SavedObjectsManagementPluginSetup } from 'src/plugins/saved_objects_management/public'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; - -import { ShareToSpaceSavedObjectsManagementAction } from './share_saved_objects_to_space_action'; -// import { ShareToSpaceSavedObjectsManagementColumn } from './share_saved_objects_to_space_column'; - -interface SetupDeps { - savedObjectsManagementSetup: SavedObjectsManagementPluginSetup; - spacesApiUi: SpacesApiUi; -} - -export class ShareSavedObjectsToSpaceService { - public setup({ savedObjectsManagementSetup, spacesApiUi }: SetupDeps) { - const action = new ShareToSpaceSavedObjectsManagementAction(spacesApiUi); - savedObjectsManagementSetup.actions.register(action); - // Note: this column is hidden for now because no saved objects are shareable. It should be uncommented when at least one saved object type is multi-namespace. - // const column = new ShareToSpaceSavedObjectsManagementColumn(spacesApiUi); - // savedObjectsManagementSetup.columns.register(column); - } -} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6e80cb781e9a6..04eff2c9936f5 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23267,8 +23267,6 @@ "xpack.spaces.management.confirmDeleteModal.errorMessage": "スペース'{name}'を削除できませんでした", "xpack.spaces.management.confirmDeleteModal.successMessage": "スペース'{name}'が削除されました", "xpack.spaces.management.confirmDeleteModal.title": "スペース'{name}'を削除しますか?", - "xpack.spaces.management.copyToSpace.actionDescription": "1つ以上のスペースでこの保存されたオブジェクトのコピーを作成します", - "xpack.spaces.management.copyToSpace.actionTitle": "スペースにコピー", "xpack.spaces.management.copyToSpace.cancelButton": "キャンセル", "xpack.spaces.management.copyToSpace.copyDetail.overwriteSwitch": "上書きしますか?", "xpack.spaces.management.copyToSpace.copyDetail.selectControlLabel": "オブジェクトID", @@ -23377,13 +23375,9 @@ "xpack.spaces.navControl.spacesMenu.changeCurrentSpaceTitle": "現在のスペースの変更", "xpack.spaces.navControl.spacesMenu.findSpacePlaceholder": "スペースを検索", "xpack.spaces.navControl.spacesMenu.noSpacesFoundTitle": " スペースが見つかりません ", - "xpack.spaces.shareToSpace.actionDescription": "この保存されたオブジェクトを1つ以上のスペースと共有します。", - "xpack.spaces.shareToSpace.actionTitle": "スペースと共有", "xpack.spaces.shareToSpace.aliasTableCalloutTitle": "レガシーURL競合", "xpack.spaces.shareToSpace.allSpacesTarget": "すべてのスペース", "xpack.spaces.shareToSpace.cancelButton": "キャンセル", - "xpack.spaces.shareToSpace.columnDescription": "このオブジェクトが現在共有されている他のスペース", - "xpack.spaces.shareToSpace.columnTitle": "共有されているスペース", "xpack.spaces.shareToSpace.continueButton": "続行", "xpack.spaces.shareToSpace.currentSpaceBadge": "現在", "xpack.spaces.shareToSpace.featureIsDisabledTooltip": "この機能はこのスペースでは無効です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5687f9e4a2f11..f3a62e21d6979 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -23796,8 +23796,6 @@ "xpack.spaces.management.confirmDeleteModal.errorMessage": "无法删除工作区“{name}”", "xpack.spaces.management.confirmDeleteModal.successMessage": "已删除工作区“{name}”", "xpack.spaces.management.confirmDeleteModal.title": "删除工作区“{name}”?", - "xpack.spaces.management.copyToSpace.actionDescription": "在一个或多个工作区中创建此已保存对象的副本", - "xpack.spaces.management.copyToSpace.actionTitle": "复制到工作区", "xpack.spaces.management.copyToSpace.cancelButton": "取消", "xpack.spaces.management.copyToSpace.copyDetail.overwriteSwitch": "覆盖?", "xpack.spaces.management.copyToSpace.copyDetail.selectControlLabel": "对象 ID", @@ -23907,14 +23905,10 @@ "xpack.spaces.navControl.spacesMenu.changeCurrentSpaceTitle": "更改当前空间", "xpack.spaces.navControl.spacesMenu.findSpacePlaceholder": "查找工作区", "xpack.spaces.navControl.spacesMenu.noSpacesFoundTitle": " 未找到工作区 ", - "xpack.spaces.shareToSpace.actionDescription": "将此已保存对象共享到一个或多个工作区", - "xpack.spaces.shareToSpace.actionTitle": "共享到工作区", "xpack.spaces.shareToSpace.aliasTableCalloutBody": "将禁用 {aliasesToDisableCount, plural, other {# 个旧版 URL}}。", "xpack.spaces.shareToSpace.aliasTableCalloutTitle": "旧版 URL 冲突", "xpack.spaces.shareToSpace.allSpacesTarget": "所有工作区", "xpack.spaces.shareToSpace.cancelButton": "取消", - "xpack.spaces.shareToSpace.columnDescription": "目前将此对象共享到的其他工作区", - "xpack.spaces.shareToSpace.columnTitle": "共享工作区", "xpack.spaces.shareToSpace.continueButton": "继续", "xpack.spaces.shareToSpace.currentSpaceBadge": "当前", "xpack.spaces.shareToSpace.featureIsDisabledTooltip": "此功能在此工作区中已禁用。", From 22c1b1cb50699de566db3f1f2455102d38b321bf Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 18 Aug 2021 13:18:49 -0400 Subject: [PATCH 05/28] Remove spacesOss plugin --- .eslintrc.js | 1 - .github/CODEOWNERS | 1 - api_docs/spaces_oss.json | 1320 ----------------- api_docs/spaces_oss.mdx | 41 - docs/developer/plugin-list.asciidoc | 4 - packages/kbn-optimizer/limits.yml | 1 - .../public/services/action_service.test.ts | 4 +- ...are_saved_objects_to_space_action.test.tsx | 4 +- .../public/services/column_service.test.ts | 4 +- src/plugins/spaces_oss/README.md | 3 - src/plugins/spaces_oss/common/index.ts | 9 - src/plugins/spaces_oss/common/types.ts | 62 - src/plugins/spaces_oss/jest.config.js | 13 - src/plugins/spaces_oss/kibana.json | 13 - src/plugins/spaces_oss/public/api.ts | 352 ----- src/plugins/spaces_oss/public/index.ts | 33 - src/plugins/spaces_oss/public/mocks/index.ts | 24 - src/plugins/spaces_oss/public/plugin.test.ts | 54 - src/plugins/spaces_oss/public/plugin.ts | 42 - src/plugins/spaces_oss/public/types.ts | 48 - src/plugins/spaces_oss/tsconfig.json | 16 - .../job_spaces_list/job_spaces_list.tsx | 3 +- .../jobs_list_page/jobs_list_page.tsx | 3 +- .../roles/edit_role/edit_role_page.tsx | 3 +- .../kibana/kibana_privileges_region.tsx | 3 +- .../privilege_summary/privilege_summary.tsx | 3 +- .../privilege_summary_table.tsx | 3 +- .../privilege_summary/space_column_header.tsx | 3 +- .../space_aware_privilege_section.tsx | 3 +- .../spaces_popover_list.test.tsx | 2 +- .../spaces_popover_list.tsx | 3 +- x-pack/plugins/spaces/common/index.ts | 1 + .../spaces/common/is_reserved_space.test.ts | 3 +- .../spaces/common/is_reserved_space.ts | 2 +- x-pack/plugins/spaces/common/types.ts | 55 +- x-pack/plugins/spaces/kibana.json | 2 +- .../advanced_settings_service.tsx | 2 +- .../advanced_settings_subtitle.tsx | 3 +- .../advanced_settings_title.tsx | 2 +- .../components/copy_to_space_flyout.tsx | 2 +- .../copy_to_space_flyout_internal.test.tsx | 4 +- .../copy_to_space_flyout_internal.tsx | 3 +- .../components/copy_to_space_form.tsx | 3 +- .../components/processing_copy_to_space.tsx | 3 +- .../copy_saved_objects_to_space/index.ts | 1 + .../lib/summarize_copy_result.test.ts | 2 +- .../lib/summarize_copy_result.ts | 3 +- .../copy_saved_objects_to_space/types.ts | 44 + x-pack/plugins/spaces/public/index.ts | 28 +- .../confirm_delete_modal.tsx | 2 +- .../edit_space/delete_spaces_button.tsx | 2 +- .../enabled_features/enabled_features.tsx | 2 +- .../enabled_features/feature_table.tsx | 2 +- .../edit_space/manage_space_page.tsx | 2 +- .../edit_space/reserved_space_badge.tsx | 2 +- .../public/management/lib/feature_utils.ts | 3 +- .../spaces_grid/spaces_grid_page.tsx | 2 +- .../management/spaces_management_app.tsx | 2 +- .../plugins/spaces/public/mocks.ts | 15 +- .../nav_control/components/spaces_menu.tsx | 2 +- .../nav_control/nav_control_popover.tsx | 2 +- x-pack/plugins/spaces/public/plugin.test.ts | 14 +- x-pack/plugins/spaces/public/plugin.tsx | 5 +- .../components/legacy_url_conflict.tsx | 3 +- .../legacy_url_conflict_internal.tsx | 2 +- .../components/relatives_footer.tsx | 3 +- .../components/share_to_space_flyout.tsx | 2 +- .../share_to_space_flyout_internal.test.tsx | 2 +- .../share_to_space_flyout_internal.tsx | 10 +- .../share_saved_objects_to_space/index.ts | 5 + .../share_saved_objects_to_space/types.ts | 123 ++ .../utils/redirect_legacy_url.ts | 2 +- .../spaces/public/space_avatar/index.ts | 1 + .../public/space_avatar/space_attributes.ts | 3 +- .../public/space_avatar/space_avatar.tsx | 2 +- .../space_avatar/space_avatar_internal.tsx | 18 +- .../spaces/public/space_avatar/types.ts | 36 + .../plugins/spaces/public/space_list/index.ts | 1 + .../spaces/public/space_list/space_list.tsx | 2 +- .../space_list/space_list_internal.test.tsx | 4 +- .../public/space_list/space_list_internal.tsx | 2 +- .../plugins/spaces/public/space_list/types.ts | 31 + .../space_selector/components/space_card.tsx | 3 +- .../space_selector/components/space_cards.tsx | 3 +- .../space_selector/space_selector.test.tsx | 2 +- .../public/space_selector/space_selector.tsx | 2 +- .../spaces/public/spaces_context/index.ts | 1 + .../spaces/public/spaces_context/types.ts | 10 + .../spaces/public/spaces_context/wrapper.tsx | 4 +- .../spaces_context/wrapper_internal.tsx | 3 +- .../spaces_manager/spaces_manager.mock.ts | 3 +- .../public/spaces_manager/spaces_manager.ts | 8 +- x-pack/plugins/spaces/public/types.ts | 26 +- .../spaces/public/ui_api/components.tsx | 2 +- x-pack/plugins/spaces/public/ui_api/index.ts | 4 +- x-pack/plugins/spaces/public/ui_api/mocks.ts | 30 - x-pack/plugins/spaces/public/ui_api/types.ts | 106 ++ .../capabilities_switcher.test.ts | 2 +- .../capabilities/capabilities_switcher.ts | 2 +- x-pack/plugins/spaces/server/index.ts | 6 +- .../on_post_auth_interceptor.ts | 2 +- .../server/routes/api/external/get_all.ts | 2 +- .../spaces/server/routes/api/external/put.ts | 2 +- .../lib/convert_saved_object_to_space.ts | 2 +- .../migrations/space_migrations.test.ts | 3 +- .../migrations/space_migrations.ts | 3 +- .../spaces_client/spaces_client.mock.ts | 3 +- .../server/spaces_client/spaces_client.ts | 2 +- .../server/spaces_service/spaces_service.ts | 2 +- x-pack/plugins/spaces/tsconfig.json | 1 - 110 files changed, 565 insertions(+), 2224 deletions(-) delete mode 100644 api_docs/spaces_oss.json delete mode 100644 api_docs/spaces_oss.mdx delete mode 100644 src/plugins/spaces_oss/README.md delete mode 100644 src/plugins/spaces_oss/common/index.ts delete mode 100644 src/plugins/spaces_oss/common/types.ts delete mode 100644 src/plugins/spaces_oss/jest.config.js delete mode 100644 src/plugins/spaces_oss/kibana.json delete mode 100644 src/plugins/spaces_oss/public/api.ts delete mode 100644 src/plugins/spaces_oss/public/index.ts delete mode 100644 src/plugins/spaces_oss/public/mocks/index.ts delete mode 100644 src/plugins/spaces_oss/public/plugin.test.ts delete mode 100644 src/plugins/spaces_oss/public/plugin.ts delete mode 100644 src/plugins/spaces_oss/public/types.ts delete mode 100644 src/plugins/spaces_oss/tsconfig.json rename src/plugins/spaces_oss/public/api.mock.ts => x-pack/plugins/spaces/public/mocks.ts (72%) create mode 100644 x-pack/plugins/spaces/public/space_avatar/types.ts create mode 100644 x-pack/plugins/spaces/public/space_list/types.ts delete mode 100644 x-pack/plugins/spaces/public/ui_api/mocks.ts create mode 100644 x-pack/plugins/spaces/public/ui_api/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index 2c55dd2528ef1..06fd805a02aa7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1567,7 +1567,6 @@ module.exports = { { files: [ 'src/plugins/security_oss/**/*.{js,mjs,ts,tsx}', - 'src/plugins/spaces_oss/**/*.{js,mjs,ts,tsx}', 'src/plugins/interactive_setup/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/encrypted_saved_objects/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/security/**/*.{js,mjs,ts,tsx}', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b47c3b09cce30..eaf469a6a6c99 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -280,7 +280,6 @@ # Security /src/core/server/csp/ @elastic/kibana-security @elastic/kibana-core /src/plugins/security_oss/ @elastic/kibana-security -/src/plugins/spaces_oss/ @elastic/kibana-security /src/plugins/interactive_setup/ @elastic/kibana-security /test/security_functional/ @elastic/kibana-security /x-pack/plugins/spaces/ @elastic/kibana-security diff --git a/api_docs/spaces_oss.json b/api_docs/spaces_oss.json deleted file mode 100644 index cd59756b548b6..0000000000000 --- a/api_docs/spaces_oss.json +++ /dev/null @@ -1,1320 +0,0 @@ -{ - "id": "spacesOss", - "client": { - "classes": [], - "functions": [], - "interfaces": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.LegacyUrlConflictProps", - "type": "Interface", - "tags": [], - "label": "LegacyUrlConflictProps", - "description": [ - "\nProperties for the LegacyUrlConflict component." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.LegacyUrlConflictProps.objectNoun", - "type": "string", - "tags": [], - "label": "objectNoun", - "description": [ - "\nThe string that is used to describe the object in the callout, e.g., _There is a legacy URL for this page that points to a different\n**object**_.\n\nDefault value is 'object'." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.LegacyUrlConflictProps.currentObjectId", - "type": "string", - "tags": [], - "label": "currentObjectId", - "description": [ - "\nThe ID of the object that is currently shown on the page." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.LegacyUrlConflictProps.otherObjectId", - "type": "string", - "tags": [], - "label": "otherObjectId", - "description": [ - "\nThe ID of the other object that the legacy URL alias points to." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.LegacyUrlConflictProps.otherObjectPath", - "type": "string", - "tags": [], - "label": "otherObjectPath", - "description": [ - "\nThe path to use for the new URL, optionally including `search` and/or `hash` URL components." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps", - "type": "Interface", - "tags": [], - "label": "ShareToSpaceFlyoutProps", - "description": [ - "\nProperties for the ShareToSpaceFlyout." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.savedObjectTarget", - "type": "Object", - "tags": [], - "label": "savedObjectTarget", - "description": [ - "\nThe object to render the flyout for." - ], - "signature": [ - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.ShareToSpaceSavedObjectTarget", - "text": "ShareToSpaceSavedObjectTarget" - } - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.flyoutIcon", - "type": "string", - "tags": [], - "label": "flyoutIcon", - "description": [ - "\nThe EUI icon that is rendered in the flyout's title.\n\nDefault is 'share'." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.flyoutTitle", - "type": "string", - "tags": [], - "label": "flyoutTitle", - "description": [ - "\nThe string that is rendered in the flyout's title.\n\nDefault is 'Edit spaces for object'." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.enableCreateCopyCallout", - "type": "CompoundType", - "tags": [], - "label": "enableCreateCopyCallout", - "description": [ - "\nWhen enabled, if the object is not yet shared to multiple spaces, a callout will be displayed that suggests the user might want to\ncreate a copy instead.\n\nDefault value is false." - ], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.enableCreateNewSpaceLink", - "type": "CompoundType", - "tags": [], - "label": "enableCreateNewSpaceLink", - "description": [ - "\nWhen enabled, if no other spaces exist _and_ the user has the appropriate privileges, a sentence will be displayed that suggests the\nuser might want to create a space.\n\nDefault value is false." - ], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.behaviorContext", - "type": "CompoundType", - "tags": [], - "label": "behaviorContext", - "description": [ - "\nWhen set to 'within-space' (default), the flyout behaves like it is running on a page within the active space, and it will prevent the\nuser from removing the object from the active space.\n\nConversely, when set to 'outside-space', the flyout behaves like it is running on a page outside of any space, so it will allow the\nuser to remove the object from the active space." - ], - "signature": [ - "\"within-space\" | \"outside-space\" | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.changeSpacesHandler", - "type": "Function", - "tags": [], - "label": "changeSpacesHandler", - "description": [ - "\nOptional handler that is called when the user has saved changes and there are spaces to be added to and/or removed from the object and\nits relatives. If this is not defined, a default handler will be used that calls `/api/spaces/_update_objects_spaces` and displays a\ntoast indicating what occurred." - ], - "signature": [ - "((objects: { type: string; id: string; }[], spacesToAdd: string[], spacesToRemove: string[]) => Promise) | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.changeSpacesHandler.$1", - "type": "Array", - "tags": [], - "label": "objects", - "description": [], - "signature": [ - "{ type: string; id: string; }[]" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "isRequired": true - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.changeSpacesHandler.$2", - "type": "Array", - "tags": [], - "label": "spacesToAdd", - "description": [], - "signature": [ - "string[]" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "isRequired": true - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.changeSpacesHandler.$3", - "type": "Array", - "tags": [], - "label": "spacesToRemove", - "description": [], - "signature": [ - "string[]" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [] - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.onUpdate", - "type": "Function", - "tags": [], - "label": "onUpdate", - "description": [ - "\nOptional callback when the target object and its relatives are updated." - ], - "signature": [ - "((updatedObjects: { type: string; id: string; }[]) => void) | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.onUpdate.$1", - "type": "Array", - "tags": [], - "label": "updatedObjects", - "description": [], - "signature": [ - "{ type: string; id: string; }[]" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [] - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceFlyoutProps.onClose", - "type": "Function", - "tags": [], - "label": "onClose", - "description": [ - "\nOptional callback when the flyout is closed." - ], - "signature": [ - "(() => void) | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [], - "returnComment": [] - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceSavedObjectTarget", - "type": "Interface", - "tags": [], - "label": "ShareToSpaceSavedObjectTarget", - "description": [ - "\nDescribes the target saved object during a share operation." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceSavedObjectTarget.type", - "type": "string", - "tags": [], - "label": "type", - "description": [ - "\nThe object's type." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceSavedObjectTarget.id", - "type": "string", - "tags": [], - "label": "id", - "description": [ - "\nThe object's ID." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceSavedObjectTarget.namespaces", - "type": "Array", - "tags": [], - "label": "namespaces", - "description": [ - "\nThe namespaces that the object currently exists in." - ], - "signature": [ - "string[]" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceSavedObjectTarget.icon", - "type": "string", - "tags": [], - "label": "icon", - "description": [ - "\nThe EUI icon that is rendered in the flyout's subtitle.\n\nDefault is 'empty'." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceSavedObjectTarget.title", - "type": "string", - "tags": [], - "label": "title", - "description": [ - "\nThe string that is rendered in the flyout's subtitle.\n\nDefault is `${type} [id=${id}]`." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.ShareToSpaceSavedObjectTarget.noun", - "type": "string", - "tags": [], - "label": "noun", - "description": [ - "\nThe string that is used to describe the object in several places, e.g., _Make **object** available in selected spaces only_.\n\nDefault value is 'object'." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpaceAvatarProps", - "type": "Interface", - "tags": [], - "label": "SpaceAvatarProps", - "description": [ - "\nProperties for the SpaceAvatar component." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpaceAvatarProps.space", - "type": "Object", - "tags": [], - "label": "space", - "description": [ - "The space to represent with an avatar." - ], - "signature": [ - "{ id?: string | undefined; name?: string | undefined; description?: string | undefined; color?: string | undefined; initials?: string | undefined; imageUrl?: string | undefined; disabledFeatures?: string[] | undefined; _reserved?: boolean | undefined; }" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpaceAvatarProps.size", - "type": "CompoundType", - "tags": [], - "label": "size", - "description": [ - "The size of the avatar." - ], - "signature": [ - "\"m\" | \"s\" | \"l\" | \"xl\" | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpaceAvatarProps.className", - "type": "string", - "tags": [], - "label": "className", - "description": [ - "Optional CSS class(es) to apply." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpaceAvatarProps.announceSpaceName", - "type": "CompoundType", - "tags": [], - "label": "announceSpaceName", - "description": [ - "\nWhen enabled, allows EUI to provide an aria-label for this component, which is announced on screen readers.\n\nDefault value is true." - ], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpaceAvatarProps.isDisabled", - "type": "CompoundType", - "tags": [], - "label": "isDisabled", - "description": [ - "\nWhether or not to render the avatar in a disabled state.\n\nDefault value is false." - ], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpaceListProps", - "type": "Interface", - "tags": [], - "label": "SpaceListProps", - "description": [ - "\nProperties for the SpaceList component." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpaceListProps.namespaces", - "type": "Array", - "tags": [], - "label": "namespaces", - "description": [ - "\nThe namespaces of a saved object to render into a corresponding list of spaces." - ], - "signature": [ - "string[]" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpaceListProps.displayLimit", - "type": "number", - "tags": [], - "label": "displayLimit", - "description": [ - "\nOptional limit to the number of spaces that can be displayed in the list. If the number of spaces exceeds this limit, they will be\nhidden behind a \"show more\" button. Set to 0 to disable.\n\nDefault value is 5." - ], - "signature": [ - "number | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpaceListProps.behaviorContext", - "type": "CompoundType", - "tags": [], - "label": "behaviorContext", - "description": [ - "\nWhen set to 'within-space' (default), the space list behaves like it is running on a page within the active space, and it will omit the\nactive space (e.g., it displays a list of all the _other_ spaces that an object is shared to).\n\nConversely, when set to 'outside-space', the space list behaves like it is running on a page outside of any space, so it will not omit\nthe active space." - ], - "signature": [ - "\"within-space\" | \"outside-space\" | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApi", - "type": "Interface", - "tags": [], - "label": "SpacesApi", - "description": [ - "\nClient-side Spaces API." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApi.getActiveSpace$", - "type": "Function", - "tags": [], - "label": "getActiveSpace$", - "description": [ - "\nObservable representing the currently active space.\nThe details of the space can change without a full page reload (such as display name, color, etc.)" - ], - "signature": [ - "() => ", - "Observable", - "<", - { - "pluginId": "spacesOss", - "scope": "common", - "docId": "kibSpacesOssPluginApi", - "section": "def-common.Space", - "text": "Space" - }, - ">" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApi.getActiveSpace", - "type": "Function", - "tags": [], - "label": "getActiveSpace", - "description": [ - "\nRetrieve the currently active space." - ], - "signature": [ - "() => Promise<", - { - "pluginId": "spacesOss", - "scope": "common", - "docId": "kibSpacesOssPluginApi", - "section": "def-common.Space", - "text": "Space" - }, - ">" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [], - "returnComment": [] - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApi.ui", - "type": "Object", - "tags": [], - "label": "ui", - "description": [ - "\nUI components and services to add spaces capabilities to an application." - ], - "signature": [ - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpacesApiUi", - "text": "SpacesApiUi" - } - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUi", - "type": "Interface", - "tags": [], - "label": "SpacesApiUi", - "description": [ - "\nUI components and services to add spaces capabilities to an application." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUi.components", - "type": "Object", - "tags": [], - "label": "components", - "description": [ - "\nLazy-loadable {@link SpacesApiUiComponent | React components} to support the Spaces feature." - ], - "signature": [ - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpacesApiUiComponent", - "text": "SpacesApiUiComponent" - } - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUi.redirectLegacyUrl", - "type": "Function", - "tags": [], - "label": "redirectLegacyUrl", - "description": [ - "\nRedirect the user from a legacy URL to a new URL. This needs to be used if a call to `SavedObjectsClient.resolve()` results in an\n`\"aliasMatch\"` outcome, which indicates that the user has loaded the page using a legacy URL. Calling this function will trigger a\nclient-side redirect to the new URL, and it will display a toast to the user.\n\nConsumers need to determine the local path for the new URL on their own, based on the object ID that was used to call\n`SavedObjectsClient.resolve()` (old ID) and the object ID in the result (new ID). For example...\n\nThe old object ID is `workpad-123` and the new object ID is `workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e`.\n\nFull legacy URL: `https://localhost:5601/app/canvas#/workpad/workpad-123/page/1`\n\nNew URL path: `#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1`\n\nThe protocol, hostname, port, base path, and app path are automatically included.\n" - ], - "signature": [ - "(path: string, objectNoun?: string | undefined) => Promise" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUi.redirectLegacyUrl.$1", - "type": "string", - "tags": [], - "label": "path", - "description": [ - "The path to use for the new URL, optionally including `search` and/or `hash` URL components." - ], - "signature": [ - "string" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "isRequired": true - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUi.redirectLegacyUrl.$2", - "type": "string", - "tags": [], - "label": "objectNoun", - "description": [ - "The string that is used to describe the object in the toast, e.g., _The **object** you're looking for has a new\nlocation_. Default value is 'object'." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "isRequired": false - } - ], - "returnComment": [] - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUiComponent", - "type": "Interface", - "tags": [], - "label": "SpacesApiUiComponent", - "description": [ - "\nReact UI components to be used to display the Spaces feature in any application." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUiComponent.getSpacesContextProvider", - "type": "Function", - "tags": [], - "label": "getSpacesContextProvider", - "description": [ - "\nProvides a context that is required to render some Spaces components." - ], - "signature": [ - "(props: ", - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpacesContextProps", - "text": "SpacesContextProps" - }, - ") => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.props", - "type": "Uncategorized", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "T" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ] - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUiComponent.getShareToSpaceFlyout", - "type": "Function", - "tags": [], - "label": "getShareToSpaceFlyout", - "description": [ - "\nDisplays a flyout to edit the spaces that an object is shared to.\n\nNote: must be rendered inside of a SpacesContext." - ], - "signature": [ - "(props: ", - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.ShareToSpaceFlyoutProps", - "text": "ShareToSpaceFlyoutProps" - }, - ") => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.props", - "type": "Uncategorized", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "T" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ] - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUiComponent.getSpaceList", - "type": "Function", - "tags": [], - "label": "getSpaceList", - "description": [ - "\nDisplays a corresponding list of spaces for a given list of saved object namespaces. It shows up to five spaces (and an indicator for\nany number of spaces that the user is not authorized to see) by default. If more than five named spaces would be displayed, the extras\n(along with the unauthorized spaces indicator, if present) are hidden behind a button. If '*' (aka \"All spaces\") is present, it\nsupersedes all of the above and just displays a single badge without a button.\n\nNote: must be rendered inside of a SpacesContext." - ], - "signature": [ - "(props: ", - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpaceListProps", - "text": "SpaceListProps" - }, - ") => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.props", - "type": "Uncategorized", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "T" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ] - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUiComponent.getLegacyUrlConflict", - "type": "Function", - "tags": [], - "label": "getLegacyUrlConflict", - "description": [ - "\nDisplays a callout that needs to be used if a call to `SavedObjectsClient.resolve()` results in an `\"conflict\"` outcome, which\nindicates that the user has loaded the page which is associated directly with one object (A), *and* with a legacy URL that points to a\ndifferent object (B).\n\nIn this case, `SavedObjectsClient.resolve()` has returned object A. This component displays a warning callout to the user explaining\nthat there is a conflict, and it includes a button that will redirect the user to object B when clicked.\n\nConsumers need to determine the local path for the new URL on their own, based on the object ID that was used to call\n`SavedObjectsClient.resolve()` (A) and the `aliasTargetId` value in the response (B). For example...\n\nA is `workpad-123` and B is `workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e`.\n\nFull legacy URL: `https://localhost:5601/app/canvas#/workpad/workpad-123/page/1`\n\nNew URL path: `#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1`" - ], - "signature": [ - "(props: ", - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.LegacyUrlConflictProps", - "text": "LegacyUrlConflictProps" - }, - ") => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.props", - "type": "Uncategorized", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "T" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ] - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesApiUiComponent.getSpaceAvatar", - "type": "Function", - "tags": [], - "label": "getSpaceAvatar", - "description": [ - "\nDisplays an avatar for the given space." - ], - "signature": [ - "(props: ", - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpaceAvatarProps", - "text": "SpaceAvatarProps" - }, - ") => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.props", - "type": "Uncategorized", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "T" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ] - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesAvailableStartContract", - "type": "Interface", - "tags": [], - "label": "SpacesAvailableStartContract", - "description": [ - "\nOSS Spaces plugin start contract when the Spaces feature is enabled." - ], - "signature": [ - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpacesAvailableStartContract", - "text": "SpacesAvailableStartContract" - }, - " extends ", - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpacesApi", - "text": "SpacesApi" - } - ], - "path": "src/plugins/spaces_oss/public/types.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesAvailableStartContract.isSpacesAvailable", - "type": "boolean", - "tags": [], - "label": "isSpacesAvailable", - "description": [ - "Indicates if the Spaces feature is enabled." - ], - "signature": [ - "true" - ], - "path": "src/plugins/spaces_oss/public/types.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesContextProps", - "type": "Interface", - "tags": [], - "label": "SpacesContextProps", - "description": [ - "\nProperties for the SpacesContext." - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesContextProps.feature", - "type": "string", - "tags": [], - "label": "feature", - "description": [ - "\nIf a feature is specified, all Spaces components will treat it appropriately if the feature is disabled in a given Space." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesUnavailableStartContract", - "type": "Interface", - "tags": [ - "deprecated" - ], - "label": "SpacesUnavailableStartContract", - "description": [ - "\nOSS Spaces plugin start contract when the Spaces feature is disabled." - ], - "path": "src/plugins/spaces_oss/public/types.ts", - "deprecated": true, - "removeBy": "8.0", - "references": [], - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesUnavailableStartContract.isSpacesAvailable", - "type": "boolean", - "tags": [], - "label": "isSpacesAvailable", - "description": [ - "Indicates if the Spaces feature is enabled." - ], - "signature": [ - "false" - ], - "path": "src/plugins/spaces_oss/public/types.ts", - "deprecated": false - } - ], - "initialIsOpen": false - } - ], - "enums": [], - "misc": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.LazyComponentFn", - "type": "Type", - "tags": [], - "label": "LazyComponentFn", - "description": [ - "\nFunction that returns a promise for a lazy-loadable component." - ], - "signature": [ - "(props: T) => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.props", - "type": "Uncategorized", - "tags": [], - "label": "props", - "description": [], - "signature": [ - "T" - ], - "path": "src/plugins/spaces_oss/public/api.ts", - "deprecated": false - } - ], - "initialIsOpen": false - } - ], - "objects": [], - "setup": { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesOssPluginSetup", - "type": "Interface", - "tags": [], - "label": "SpacesOssPluginSetup", - "description": [ - "\nOSS Spaces plugin setup contract." - ], - "path": "src/plugins/spaces_oss/public/types.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesOssPluginSetup.registerSpacesApi", - "type": "Function", - "tags": [ - "private" - ], - "label": "registerSpacesApi", - "description": [ - "\nRegister a provider for the Spaces API.\n\nOnly one provider can be registered, subsequent calls to this method will fail.\n" - ], - "signature": [ - "(provider: ", - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpacesApi", - "text": "SpacesApi" - }, - ") => void" - ], - "path": "src/plugins/spaces_oss/public/types.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesOssPluginSetup.registerSpacesApi.$1", - "type": "Object", - "tags": [], - "label": "provider", - "description": [ - "the API provider." - ], - "signature": [ - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpacesApi", - "text": "SpacesApi" - } - ], - "path": "src/plugins/spaces_oss/public/types.ts", - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [] - } - ], - "lifecycle": "setup", - "initialIsOpen": true - }, - "start": { - "parentPluginId": "spacesOss", - "id": "def-public.SpacesOssPluginStart", - "type": "Type", - "tags": [], - "label": "SpacesOssPluginStart", - "description": [ - "\nOSS Spaces plugin start contract." - ], - "signature": [ - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpacesAvailableStartContract", - "text": "SpacesAvailableStartContract" - }, - " | ", - { - "pluginId": "spacesOss", - "scope": "public", - "docId": "kibSpacesOssPluginApi", - "section": "def-public.SpacesUnavailableStartContract", - "text": "SpacesUnavailableStartContract" - } - ], - "path": "src/plugins/spaces_oss/public/types.ts", - "deprecated": false, - "lifecycle": "start", - "initialIsOpen": true - } - }, - "server": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [], - "objects": [] - }, - "common": { - "classes": [], - "functions": [], - "interfaces": [ - { - "parentPluginId": "spacesOss", - "id": "def-common.Space", - "type": "Interface", - "tags": [], - "label": "Space", - "description": [ - "\nA Space." - ], - "path": "src/plugins/spaces_oss/common/types.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "spacesOss", - "id": "def-common.Space.id", - "type": "string", - "tags": [], - "label": "id", - "description": [ - "\nThe unique identifier for this space.\nThe id becomes part of the \"URL Identifier\" of the space.\n\nExample: an id of `marketing` would result in the URL identifier of `/s/marketing`." - ], - "path": "src/plugins/spaces_oss/common/types.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-common.Space.name", - "type": "string", - "tags": [], - "label": "name", - "description": [ - "\nDisplay name for this space." - ], - "path": "src/plugins/spaces_oss/common/types.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-common.Space.description", - "type": "string", - "tags": [], - "label": "description", - "description": [ - "\nOptional description for this space." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/common/types.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-common.Space.color", - "type": "string", - "tags": [], - "label": "color", - "description": [ - "\nOptional color (hex code) for this space.\nIf neither `color` nor `imageUrl` is specified, then a color will be automatically generated." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/common/types.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-common.Space.initials", - "type": "string", - "tags": [], - "label": "initials", - "description": [ - "\nOptional display initials for this space's avatar. Supports a maximum of 2 characters.\nIf initials are not provided, then they will be derived from the space name automatically.\n\nInitials are not displayed if an `imageUrl` has been specified." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/common/types.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-common.Space.imageUrl", - "type": "string", - "tags": [], - "label": "imageUrl", - "description": [ - "\nOptional base-64 encoded data image url to show as this space's avatar.\nThis setting takes precedence over any configured `color` or `initials`." - ], - "signature": [ - "string | undefined" - ], - "path": "src/plugins/spaces_oss/common/types.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-common.Space.disabledFeatures", - "type": "Array", - "tags": [], - "label": "disabledFeatures", - "description": [ - "\nThe set of feature ids that should be hidden within this space." - ], - "signature": [ - "string[]" - ], - "path": "src/plugins/spaces_oss/common/types.ts", - "deprecated": false - }, - { - "parentPluginId": "spacesOss", - "id": "def-common.Space._reserved", - "type": "CompoundType", - "tags": [ - "private" - ], - "label": "_reserved", - "description": [ - "\nIndicates that this space is reserved (system controlled).\nReserved spaces cannot be created or deleted by end-users." - ], - "signature": [ - "boolean | undefined" - ], - "path": "src/plugins/spaces_oss/common/types.ts", - "deprecated": false - } - ], - "initialIsOpen": false - } - ], - "enums": [], - "misc": [], - "objects": [] - } -} \ No newline at end of file diff --git a/api_docs/spaces_oss.mdx b/api_docs/spaces_oss.mdx deleted file mode 100644 index d166a37a9373a..0000000000000 --- a/api_docs/spaces_oss.mdx +++ /dev/null @@ -1,41 +0,0 @@ ---- -id: kibSpacesOssPluginApi -slug: /kibana-dev-docs/spacesOssPluginApi -title: spacesOss -image: https://source.unsplash.com/400x175/?github -summary: API docs for the spacesOss plugin -date: 2020-11-16 -tags: ['contributor', 'dev', 'apidocs', 'kibana', 'spacesOss'] -warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. ---- -import spacesOssObj from './spaces_oss.json'; - -This plugin exposes a limited set of spaces functionality to OSS plugins. - -Contact [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) for questions regarding this plugin. - -**Code health stats** - -| Public API count | Any count | Items lacking comments | Missing exports | -|-------------------|-----------|------------------------|-----------------| -| 77 | 0 | 10 | 0 | - -## Client - -### Setup - - -### Start - - -### Interfaces - - -### Consts, variables and types - - -## Common - -### Interfaces - - diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 6431d85ac1a51..fa8fcb20cc2ea 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -223,10 +223,6 @@ so they can properly protect the data within their clusters. generating deep links to other apps, and creating short URLs. -|{kib-repo}blob/{branch}/src/plugins/spaces_oss/README.md[spacesOss] -|Bridge plugin for consumption of the Spaces feature from OSS plugins. - - |{kib-repo}blob/{branch}/src/plugins/telemetry/README.md[telemetry] |Telemetry allows Kibana features to have usage tracked in the wild. The general term "telemetry" refers to multiple things: diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index b11458d6539e8..b142bbffc8ad7 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -99,7 +99,6 @@ pageLoadAssetSize: runtimeFields: 41752 stackAlerts: 29684 presentationUtil: 94301 - spacesOss: 18817 indexPatternFieldEditor: 50000 osquery: 107090 fileUpload: 25664 diff --git a/src/plugins/saved_objects_management/public/services/action_service.test.ts b/src/plugins/saved_objects_management/public/services/action_service.test.ts index a86c27a13e6f2..7a2536611f58a 100644 --- a/src/plugins/saved_objects_management/public/services/action_service.test.ts +++ b/src/plugins/saved_objects_management/public/services/action_service.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { spacesApiMock } from '../../../spaces_oss/public/api.mock'; +import { spacesPluginMock } from '../../../../../x-pack/plugins/spaces/public/mocks'; import { CopyToSpaceSavedObjectsManagementAction, ShareToSpaceSavedObjectsManagementAction, @@ -49,7 +49,7 @@ describe('SavedObjectsManagementActionRegistry', () => { it('allows actions to be registered and retrieved', () => { const action = createAction('foo'); setup.register(action); - const start = service.start(spacesApiMock.create()); + const start = service.start(spacesPluginMock.createStartContract()); expect(start.getAll()).toEqual([ action, expect.any(ShareToSpaceSavedObjectsManagementAction), diff --git a/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.test.tsx b/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.test.tsx index 61a4627e030f9..235f8d4508a64 100644 --- a/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.test.tsx +++ b/src/plugins/saved_objects_management/public/services/actions/share_saved_objects_to_space_action.test.tsx @@ -6,13 +6,13 @@ * Side Public License, v 1. */ -import { uiApiMock } from '../../../../../../x-pack/plugins/spaces/public/ui_api/mocks'; +import { spacesPluginMock } from '../../../../../../x-pack/plugins/spaces/public/mocks'; import type { SavedObjectsManagementRecord } from '../types'; import { ShareToSpaceSavedObjectsManagementAction } from './share_saved_objects_to_space_action'; describe('ShareToSpaceSavedObjectsManagementAction', () => { const createAction = () => { - const spacesApiUi = uiApiMock.create(); + const { ui: spacesApiUi } = spacesPluginMock.createStartContract(); return new ShareToSpaceSavedObjectsManagementAction(spacesApiUi); }; describe('#euiAction.available', () => { diff --git a/src/plugins/saved_objects_management/public/services/column_service.test.ts b/src/plugins/saved_objects_management/public/services/column_service.test.ts index fcd3b6c7cd717..581a55fa0066d 100644 --- a/src/plugins/saved_objects_management/public/services/column_service.test.ts +++ b/src/plugins/saved_objects_management/public/services/column_service.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { spacesApiMock } from '../../../spaces_oss/public/api.mock'; +import { spacesPluginMock } from '../../../../../x-pack/plugins/spaces/public/mocks'; // import { ShareToSpaceSavedObjectsManagementColumn } from './columns'; import { SavedObjectsManagementColumnService, @@ -42,7 +42,7 @@ describe('SavedObjectsManagementColumnRegistry', () => { it('allows columns to be registered and retrieved', () => { const column = createColumn('foo'); setup.register(column); - const start = service.start(spacesApiMock.create()); + const start = service.start(spacesPluginMock.createStartContract()); expect(start.getAll()).toEqual([ column, // expect.any(ShareToSpaceSavedObjectsManagementColumn), diff --git a/src/plugins/spaces_oss/README.md b/src/plugins/spaces_oss/README.md deleted file mode 100644 index 73de736d6fb4e..0000000000000 --- a/src/plugins/spaces_oss/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# SpacesOss - -Bridge plugin for consumption of the Spaces feature from OSS plugins. diff --git a/src/plugins/spaces_oss/common/index.ts b/src/plugins/spaces_oss/common/index.ts deleted file mode 100644 index a499a06983e63..0000000000000 --- a/src/plugins/spaces_oss/common/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { Space } from './types'; diff --git a/src/plugins/spaces_oss/common/types.ts b/src/plugins/spaces_oss/common/types.ts deleted file mode 100644 index b5c418cf3177e..0000000000000 --- a/src/plugins/spaces_oss/common/types.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -/** - * A Space. - */ -export interface Space { - /** - * The unique identifier for this space. - * The id becomes part of the "URL Identifier" of the space. - * - * Example: an id of `marketing` would result in the URL identifier of `/s/marketing`. - */ - id: string; - - /** - * Display name for this space. - */ - name: string; - - /** - * Optional description for this space. - */ - description?: string; - - /** - * Optional color (hex code) for this space. - * If neither `color` nor `imageUrl` is specified, then a color will be automatically generated. - */ - color?: string; - - /** - * Optional display initials for this space's avatar. Supports a maximum of 2 characters. - * If initials are not provided, then they will be derived from the space name automatically. - * - * Initials are not displayed if an `imageUrl` has been specified. - */ - initials?: string; - - /** - * Optional base-64 encoded data image url to show as this space's avatar. - * This setting takes precedence over any configured `color` or `initials`. - */ - imageUrl?: string; - - /** - * The set of feature ids that should be hidden within this space. - */ - disabledFeatures: string[]; - - /** - * Indicates that this space is reserved (system controlled). - * Reserved spaces cannot be created or deleted by end-users. - * @private - */ - _reserved?: boolean; -} diff --git a/src/plugins/spaces_oss/jest.config.js b/src/plugins/spaces_oss/jest.config.js deleted file mode 100644 index 8be5bf6e0fb54..0000000000000 --- a/src/plugins/spaces_oss/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/spaces_oss'], -}; diff --git a/src/plugins/spaces_oss/kibana.json b/src/plugins/spaces_oss/kibana.json deleted file mode 100644 index 10127634618f1..0000000000000 --- a/src/plugins/spaces_oss/kibana.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "id": "spacesOss", - "owner": { - "name": "Platform Security", - "githubTeam": "kibana-security" - }, - "description": "This plugin exposes a limited set of spaces functionality to OSS plugins.", - "version": "kibana", - "server": false, - "ui": true, - "requiredPlugins": [], - "optionalPlugins": [] -} diff --git a/src/plugins/spaces_oss/public/api.ts b/src/plugins/spaces_oss/public/api.ts deleted file mode 100644 index 561a1c245fa93..0000000000000 --- a/src/plugins/spaces_oss/public/api.ts +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { ReactElement } from 'react'; -import type { Observable } from 'rxjs'; - -import type { Space } from '../common'; - -/** - * Client-side Spaces API. - */ -export interface SpacesApi { - /** - * Observable representing the currently active space. - * The details of the space can change without a full page reload (such as display name, color, etc.) - */ - getActiveSpace$(): Observable; - - /** - * Retrieve the currently active space. - */ - getActiveSpace(): Promise; - - /** - * UI components and services to add spaces capabilities to an application. - */ - ui: SpacesApiUi; -} - -/** - * Function that returns a promise for a lazy-loadable component. - */ -export type LazyComponentFn = (props: T) => ReactElement; - -/** - * UI components and services to add spaces capabilities to an application. - */ -export interface SpacesApiUi { - /** - * Lazy-loadable {@link SpacesApiUiComponent | React components} to support the Spaces feature. - */ - components: SpacesApiUiComponent; - /** - * Redirect the user from a legacy URL to a new URL. This needs to be used if a call to `SavedObjectsClient.resolve()` results in an - * `"aliasMatch"` outcome, which indicates that the user has loaded the page using a legacy URL. Calling this function will trigger a - * client-side redirect to the new URL, and it will display a toast to the user. - * - * Consumers need to determine the local path for the new URL on their own, based on the object ID that was used to call - * `SavedObjectsClient.resolve()` (old ID) and the object ID in the result (new ID). For example... - * - * The old object ID is `workpad-123` and the new object ID is `workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e`. - * - * Full legacy URL: `https://localhost:5601/app/canvas#/workpad/workpad-123/page/1` - * - * New URL path: `#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1` - * - * The protocol, hostname, port, base path, and app path are automatically included. - * - * @param path The path to use for the new URL, optionally including `search` and/or `hash` URL components. - * @param objectNoun The string that is used to describe the object in the toast, e.g., _The **object** you're looking for has a new - * location_. Default value is 'object'. - */ - redirectLegacyUrl: (path: string, objectNoun?: string) => Promise; -} - -/** - * React UI components to be used to display the Spaces feature in any application. - */ -export interface SpacesApiUiComponent { - /** - * Provides a context that is required to render some Spaces components. - */ - getSpacesContextProvider: LazyComponentFn; - /** - * Displays a flyout to edit the spaces that an object is shared to. - * - * Note: must be rendered inside of a SpacesContext. - */ - getShareToSpaceFlyout: LazyComponentFn; - /** - * Displays a flyout to copy an object to other spaces. - * - * Note: must be rendered inside of a SpacesContext. - */ - getCopyToSpaceFlyout: LazyComponentFn; - /** - * Displays a corresponding list of spaces for a given list of saved object namespaces. It shows up to five spaces (and an indicator for - * any number of spaces that the user is not authorized to see) by default. If more than five named spaces would be displayed, the extras - * (along with the unauthorized spaces indicator, if present) are hidden behind a button. If '*' (aka "All spaces") is present, it - * supersedes all of the above and just displays a single badge without a button. - * - * Note: must be rendered inside of a SpacesContext. - */ - getSpaceList: LazyComponentFn; - /** - * Displays a callout that needs to be used if a call to `SavedObjectsClient.resolve()` results in an `"conflict"` outcome, which - * indicates that the user has loaded the page which is associated directly with one object (A), *and* with a legacy URL that points to a - * different object (B). - * - * In this case, `SavedObjectsClient.resolve()` has returned object A. This component displays a warning callout to the user explaining - * that there is a conflict, and it includes a button that will redirect the user to object B when clicked. - * - * Consumers need to determine the local path for the new URL on their own, based on the object ID that was used to call - * `SavedObjectsClient.resolve()` (A) and the `alias_target_id` value in the response (B). For example... - * - * A is `workpad-123` and B is `workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e`. - * - * Full legacy URL: `https://localhost:5601/app/canvas#/workpad/workpad-123/page/1` - * - * New URL path: `#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1` - */ - getLegacyUrlConflict: LazyComponentFn; - /** - * Displays an avatar for the given space. - */ - getSpaceAvatar: LazyComponentFn; -} - -/** - * Properties for the SpacesContext. - */ -export interface SpacesContextProps { - /** - * If a feature is specified, all Spaces components will treat it appropriately if the feature is disabled in a given Space. - */ - feature?: string; -} - -/** - * Properties for the ShareToSpaceFlyout. - */ -export interface ShareToSpaceFlyoutProps { - /** - * The object to render the flyout for. - */ - savedObjectTarget: ShareToSpaceSavedObjectTarget; - /** - * The EUI icon that is rendered in the flyout's title. - * - * Default is 'share'. - */ - flyoutIcon?: string; - /** - * The string that is rendered in the flyout's title. - * - * Default is 'Edit spaces for object'. - */ - flyoutTitle?: string; - /** - * When enabled, if the object is not yet shared to multiple spaces, a callout will be displayed that suggests the user might want to - * create a copy instead. - * - * Default value is false. - */ - enableCreateCopyCallout?: boolean; - /** - * When enabled, if no other spaces exist _and_ the user has the appropriate privileges, a sentence will be displayed that suggests the - * user might want to create a space. - * - * Default value is false. - */ - enableCreateNewSpaceLink?: boolean; - /** - * When set to 'within-space' (default), the flyout behaves like it is running on a page within the active space, and it will prevent the - * user from removing the object from the active space. - * - * Conversely, when set to 'outside-space', the flyout behaves like it is running on a page outside of any space, so it will allow the - * user to remove the object from the active space. - */ - behaviorContext?: 'within-space' | 'outside-space'; - /** - * Optional handler that is called when the user has saved changes and there are spaces to be added to and/or removed from the object and - * its relatives. If this is not defined, a default handler will be used that calls `/api/spaces/_update_objects_spaces` and displays a - * toast indicating what occurred. - */ - changeSpacesHandler?: ( - objects: Array<{ type: string; id: string }>, - spacesToAdd: string[], - spacesToRemove: string[] - ) => Promise; - /** - * Optional callback when the target object and its relatives are updated. - */ - onUpdate?: (updatedObjects: Array<{ type: string; id: string }>) => void; - /** - * Optional callback when the flyout is closed. - */ - onClose?: () => void; -} - -/** - * Describes the target saved object during a share operation. - */ -export interface ShareToSpaceSavedObjectTarget { - /** - * The object's type. - */ - type: string; - /** - * The object's ID. - */ - id: string; - /** - * The namespaces that the object currently exists in. - */ - namespaces: string[]; - /** - * The EUI icon that is rendered in the flyout's subtitle. - * - * Default is 'empty'. - */ - icon?: string; - /** - * The string that is rendered in the flyout's subtitle. - * - * Default is `${type} [id=${id}]`. - */ - title?: string; - /** - * The string that is used to describe the object in several places, e.g., _Make **object** available in selected spaces only_. - * - * Default value is 'object'. - */ - noun?: string; -} - -/** - * Properties for the CopyToSpaceFlyout. - */ -export interface CopyToSpaceFlyoutProps { - /** - * The object to render the flyout for. - */ - savedObjectTarget: CopyToSpaceSavedObjectTarget; - /** - * Optional callback when the flyout is closed. - */ - onClose?: () => void; -} - -/** - * Describes the target saved object during a copy operation. - */ -export interface CopyToSpaceSavedObjectTarget { - /** - * The object's type. - */ - type: string; - /** - * The object's ID. - */ - id: string; - /** - * The namespaces that the object currently exists in. - */ - namespaces: string[]; - /** - * The EUI icon that is rendered in the flyout's subtitle. - * - * Default is 'apps'. - */ - icon?: string; - /** - * The string that is rendered in the flyout's subtitle. - * - * Default is `${type} [id=${id}]`. - */ - title?: string; -} - -/** - * Properties for the SpaceList component. - */ -export interface SpaceListProps { - /** - * The namespaces of a saved object to render into a corresponding list of spaces. - */ - namespaces: string[]; - /** - * Optional limit to the number of spaces that can be displayed in the list. If the number of spaces exceeds this limit, they will be - * hidden behind a "show more" button. Set to 0 to disable. - * - * Default value is 5. - */ - displayLimit?: number; - /** - * When set to 'within-space' (default), the space list behaves like it is running on a page within the active space, and it will omit the - * active space (e.g., it displays a list of all the _other_ spaces that an object is shared to). - * - * Conversely, when set to 'outside-space', the space list behaves like it is running on a page outside of any space, so it will not omit - * the active space. - */ - behaviorContext?: 'within-space' | 'outside-space'; -} - -/** - * Properties for the LegacyUrlConflict component. - */ -export interface LegacyUrlConflictProps { - /** - * The string that is used to describe the object in the callout, e.g., _There is a legacy URL for this page that points to a different - * **object**_. - * - * Default value is 'object'. - */ - objectNoun?: string; - /** - * The ID of the object that is currently shown on the page. - */ - currentObjectId: string; - /** - * The ID of the other object that the legacy URL alias points to. - */ - otherObjectId: string; - /** - * The path to use for the new URL, optionally including `search` and/or `hash` URL components. - */ - otherObjectPath: string; -} - -/** - * Properties for the SpaceAvatar component. - */ -export interface SpaceAvatarProps { - /** The space to represent with an avatar. */ - space: Partial; - - /** The size of the avatar. */ - size?: 's' | 'm' | 'l' | 'xl'; - - /** Optional CSS class(es) to apply. */ - className?: string; - - /** - * When enabled, allows EUI to provide an aria-label for this component, which is announced on screen readers. - * - * Default value is true. - */ - announceSpaceName?: boolean; - - /** - * Whether or not to render the avatar in a disabled state. - * - * Default value is false. - */ - isDisabled?: boolean; -} diff --git a/src/plugins/spaces_oss/public/index.ts b/src/plugins/spaces_oss/public/index.ts deleted file mode 100644 index 62fc3063fb078..0000000000000 --- a/src/plugins/spaces_oss/public/index.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { SpacesOssPlugin } from './plugin'; - -export type { - SpacesOssPluginSetup, - SpacesOssPluginStart, - SpacesAvailableStartContract, - SpacesUnavailableStartContract, -} from './types'; - -export type { - LazyComponentFn, - SpacesApi, - SpacesApiUi, - SpacesApiUiComponent, - SpacesContextProps, - ShareToSpaceFlyoutProps, - ShareToSpaceSavedObjectTarget, - CopyToSpaceFlyoutProps, - CopyToSpaceSavedObjectTarget, - SpaceListProps, - LegacyUrlConflictProps, - SpaceAvatarProps, -} from './api'; - -export const plugin = () => new SpacesOssPlugin(); diff --git a/src/plugins/spaces_oss/public/mocks/index.ts b/src/plugins/spaces_oss/public/mocks/index.ts deleted file mode 100644 index dc7b9e34fe822..0000000000000 --- a/src/plugins/spaces_oss/public/mocks/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { SpacesOssPluginSetup, SpacesOssPluginStart } from '../'; -import { spacesApiMock } from '../api.mock'; - -const createSetupContract = (): jest.Mocked => ({ - registerSpacesApi: jest.fn(), -}); - -const createStartContract = (): jest.Mocked => ({ - isSpacesAvailable: true, - ...spacesApiMock.create(), -}); - -export const spacesOssPluginMock = { - createSetupContract, - createStartContract, -}; diff --git a/src/plugins/spaces_oss/public/plugin.test.ts b/src/plugins/spaces_oss/public/plugin.test.ts deleted file mode 100644 index fcbe1c7d86ce5..0000000000000 --- a/src/plugins/spaces_oss/public/plugin.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { spacesApiMock } from './api.mock'; -import { SpacesOssPlugin } from './plugin'; - -describe('SpacesOssPlugin', () => { - let plugin: SpacesOssPlugin; - - beforeEach(() => { - plugin = new SpacesOssPlugin(); - }); - - describe('#setup', () => { - it('only allows the API to be registered once', async () => { - const spacesApi = spacesApiMock.create(); - const { registerSpacesApi } = plugin.setup(); - - expect(() => registerSpacesApi(spacesApi)).not.toThrow(); - - expect(() => registerSpacesApi(spacesApi)).toThrowErrorMatchingInlineSnapshot( - `"Spaces API can only be registered once"` - ); - }); - }); - - describe('#start', () => { - it('returns the spaces API if registered', async () => { - const spacesApi = spacesApiMock.create(); - const { registerSpacesApi } = plugin.setup(); - - registerSpacesApi(spacesApi); - - const { isSpacesAvailable, ...api } = plugin.start(); - - expect(isSpacesAvailable).toBe(true); - expect(api).toStrictEqual(spacesApi); - }); - - it('does not return the spaces API if not registered', async () => { - plugin.setup(); - - const { isSpacesAvailable, ...api } = plugin.start(); - - expect(isSpacesAvailable).toBe(false); - expect(Object.keys(api)).toHaveLength(0); - }); - }); -}); diff --git a/src/plugins/spaces_oss/public/plugin.ts b/src/plugins/spaces_oss/public/plugin.ts deleted file mode 100644 index 2531453257e3e..0000000000000 --- a/src/plugins/spaces_oss/public/plugin.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { Plugin } from 'src/core/public'; - -import type { SpacesApi } from './api'; -import type { SpacesOssPluginSetup, SpacesOssPluginStart } from './types'; - -export class SpacesOssPlugin implements Plugin { - private api?: SpacesApi; - - constructor() {} - - public setup() { - return { - registerSpacesApi: (provider: SpacesApi) => { - if (this.api) { - throw new Error('Spaces API can only be registered once'); - } - this.api = provider; - }, - }; - } - - public start() { - if (this.api) { - return { - isSpacesAvailable: true as true, - ...this.api!, - }; - } else { - return { - isSpacesAvailable: false as false, - }; - } - } -} diff --git a/src/plugins/spaces_oss/public/types.ts b/src/plugins/spaces_oss/public/types.ts deleted file mode 100644 index df20e9be6eaa1..0000000000000 --- a/src/plugins/spaces_oss/public/types.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import type { SpacesApi } from './api'; - -/** - * OSS Spaces plugin start contract when the Spaces feature is enabled. - */ -export interface SpacesAvailableStartContract extends SpacesApi { - /** Indicates if the Spaces feature is enabled. */ - isSpacesAvailable: true; -} - -/** - * OSS Spaces plugin start contract when the Spaces feature is disabled. - * @deprecated The Spaces plugin will always be enabled starting in 8.0. - * @removeBy 8.0 - */ -export interface SpacesUnavailableStartContract { - /** Indicates if the Spaces feature is enabled. */ - isSpacesAvailable: false; -} - -/** - * OSS Spaces plugin setup contract. - */ -export interface SpacesOssPluginSetup { - /** - * Register a provider for the Spaces API. - * - * Only one provider can be registered, subsequent calls to this method will fail. - * - * @param provider the API provider. - * - * @private designed to only be consumed by the `spaces` plugin. - */ - registerSpacesApi(provider: SpacesApi): void; -} - -/** - * OSS Spaces plugin start contract. - */ -export type SpacesOssPluginStart = SpacesAvailableStartContract | SpacesUnavailableStartContract; diff --git a/src/plugins/spaces_oss/tsconfig.json b/src/plugins/spaces_oss/tsconfig.json deleted file mode 100644 index 35942863c1f1b..0000000000000 --- a/src/plugins/spaces_oss/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./target/types", - "emitDeclarationOnly": true, - "declaration": true, - "declarationMap": true - }, - "include": [ - "common/**/*", - "public/**/*", - ], - "references": [ - { "path": "../../core/tsconfig.json" }, - ] -} diff --git a/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx b/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx index a405f0486430c..8f17591fdd64b 100644 --- a/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx +++ b/x-pack/plugins/ml/public/application/components/job_spaces_list/job_spaces_list.tsx @@ -9,13 +9,12 @@ import React, { FC, useCallback, useState } from 'react'; import { EuiButtonEmpty } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { ShareToSpaceFlyoutProps } from 'src/plugins/spaces_oss/public'; import { JobType, ML_SAVED_OBJECT_TYPE, SavedObjectResult, } from '../../../../common/types/saved_objects'; -import type { SpacesPluginStart } from '../../../../../spaces/public'; +import type { SpacesPluginStart, ShareToSpaceFlyoutProps } from '../../../../../spaces/public'; import { ml } from '../../services/ml_api_service'; import { useToastNotificationService } from '../../services/toast_notification_service'; diff --git a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx index a1528b91d5abb..8dccbe973318b 100644 --- a/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx +++ b/x-pack/plugins/ml/public/application/management/jobs_list/components/jobs_list_page/jobs_list_page.tsx @@ -22,7 +22,6 @@ import { EuiFlexItem, } from '@elastic/eui'; -import type { SpacesContextProps } from 'src/plugins/spaces_oss/public'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; import { PLUGIN_ID } from '../../../../../../common/constants/app'; @@ -41,7 +40,7 @@ import { DataFrameAnalyticsList } from '../../../../data_frame_analytics/pages/a import { AccessDeniedPage } from '../access_denied_page'; import { InsufficientLicensePage } from '../insufficient_license_page'; import type { SharePluginStart } from '../../../../../../../../../src/plugins/share/public'; -import type { SpacesPluginStart } from '../../../../../../../spaces/public'; +import type { SpacesPluginStart, SpacesContextProps } from '../../../../../../../spaces/public'; import { JobSpacesSyncFlyout } from '../../../../components/job_spaces_sync'; import { getDefaultAnomalyDetectionJobsListState } from '../../../../jobs/jobs_list/jobs'; import { getMlGlobalServices } from '../../../../app'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index ef1338ab9d971..3a66b6d80c615 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -35,12 +35,11 @@ import type { ScopedHistory, } from 'src/core/public'; import type { IndexPatternsContract } from 'src/plugins/data/public'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; import { reactRouterNavigate } from '../../../../../../../src/plugins/kibana_react/public'; import type { KibanaFeature } from '../../../../../features/common'; import type { FeaturesPluginStart } from '../../../../../features/public'; -import type { Space } from '../../../../../spaces/public'; +import type { Space, SpacesApiUi } from '../../../../../spaces/public'; import type { SecurityLicense } from '../../../../common/licensing'; import type { BuiltinESPrivileges, diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx index 486b1c8bc1d03..c9c7df222df29 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/kibana_privileges_region.tsx @@ -8,9 +8,8 @@ import React, { Component } from 'react'; import type { Capabilities } from 'src/core/public'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; -import type { Space } from '../../../../../../../spaces/public'; +import type { Space, SpacesApiUi } from '../../../../../../../spaces/public'; import type { Role } from '../../../../../../common/model'; import type { KibanaPrivileges } from '../../../model'; import { CollapsiblePanel } from '../../collapsible_panel'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx index 48a0d18653053..27bf246c0596f 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary.tsx @@ -17,9 +17,8 @@ import { import React, { Fragment, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; -import type { Space } from '../../../../../../../../spaces/public'; +import type { Space, SpacesApiUi } from '../../../../../../../../spaces/public'; import type { Role } from '../../../../../../../common/model'; import type { KibanaPrivileges } from '../../../../model'; import { PrivilegeSummaryTable } from './privilege_summary_table'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.tsx index 582a7d6c5427e..56c841f68f504 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/privilege_summary_table.tsx @@ -20,9 +20,8 @@ import { import React, { Fragment, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; -import type { Space } from '../../../../../../../../spaces/public'; +import type { Space, SpacesApiUi } from '../../../../../../../../spaces/public'; import type { Role, RoleKibanaPrivilege } from '../../../../../../../common/model'; import type { KibanaPrivileges, SecuredFeature } from '../../../../model'; import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/space_column_header.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/space_column_header.tsx index fd535d20de557..38c122ba10086 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/space_column_header.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/privilege_summary/space_column_header.tsx @@ -9,9 +9,8 @@ import React, { Fragment, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Space } from 'src/plugins/spaces_oss/common'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; +import type { Space, SpacesApiUi } from '../../../../../../../../spaces/public'; import type { RoleKibanaPrivilege } from '../../../../../../../common/model'; import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; import { SpacesPopoverList } from '../../../spaces_popover_list'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index 9ca41a018cd33..6492ca6e01af0 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -20,9 +20,8 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { Capabilities } from 'src/core/public'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; -import type { Space } from '../../../../../../../../spaces/public'; +import type { Space, SpacesApiUi } from '../../../../../../../../spaces/public'; import type { Role } from '../../../../../../../common/model'; import { isRoleReserved } from '../../../../../../../common/model'; import type { KibanaPrivileges } from '../../../../model'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx index 2925866a5752f..fb21fac3006b8 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.test.tsx @@ -17,8 +17,8 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { coreMock } from 'src/core/public/mocks'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../../../../../spaces/public'; import { SpaceAvatarInternal } from '../../../../../../spaces/public/space_avatar/space_avatar_internal'; import { spacesManagerMock } from '../../../../../../spaces/public/spaces_manager/mocks'; import { getUiApi } from '../../../../../../spaces/public/ui_api'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx index 9861b008beb9f..e715cb217ae67 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx @@ -19,10 +19,9 @@ import React, { Component, memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Space } from 'src/plugins/spaces_oss/common'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../spaces/common'; +import type { Space, SpacesApiUi } from '../../../../../../spaces/public'; interface Props { spaces: Space[]; diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index 003a0c068a166..3af87c44f8e0f 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -9,6 +9,7 @@ export { isReservedSpace } from './is_reserved_space'; export { MAX_SPACE_INITIALS, SPACE_SEARCH_COUNT_THRESHOLD, ENTER_SPACE_PATH } from './constants'; export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser'; export type { + Space, GetAllSpacesOptions, GetAllSpacesPurpose, GetSpaceResult, diff --git a/x-pack/plugins/spaces/common/is_reserved_space.test.ts b/x-pack/plugins/spaces/common/is_reserved_space.test.ts index 0128a7483f166..630d4a000f3e5 100644 --- a/x-pack/plugins/spaces/common/is_reserved_space.test.ts +++ b/x-pack/plugins/spaces/common/is_reserved_space.test.ts @@ -5,9 +5,8 @@ * 2.0. */ -import type { Space } from 'src/plugins/spaces_oss/common'; - import { isReservedSpace } from './is_reserved_space'; +import type { Space } from './types'; test('it returns true for reserved spaces', () => { const space: Space = { diff --git a/x-pack/plugins/spaces/common/is_reserved_space.ts b/x-pack/plugins/spaces/common/is_reserved_space.ts index f78fe7bbdac1b..92b9a0a99ddbb 100644 --- a/x-pack/plugins/spaces/common/is_reserved_space.ts +++ b/x-pack/plugins/spaces/common/is_reserved_space.ts @@ -7,7 +7,7 @@ import { get } from 'lodash'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from './types'; /** * Returns whether the given Space is reserved or not. diff --git a/x-pack/plugins/spaces/common/types.ts b/x-pack/plugins/spaces/common/types.ts index 55bd1c137f8cf..39864447310b4 100644 --- a/x-pack/plugins/spaces/common/types.ts +++ b/x-pack/plugins/spaces/common/types.ts @@ -5,7 +5,60 @@ * 2.0. */ -import type { Space } from 'src/plugins/spaces_oss/common'; +/** + * A Space. + */ +export interface Space { + /** + * The unique identifier for this space. + * The id becomes part of the "URL Identifier" of the space. + * + * Example: an id of `marketing` would result in the URL identifier of `/s/marketing`. + */ + id: string; + + /** + * Display name for this space. + */ + name: string; + + /** + * Optional description for this space. + */ + description?: string; + + /** + * Optional color (hex code) for this space. + * If neither `color` nor `imageUrl` is specified, then a color will be automatically generated. + */ + color?: string; + + /** + * Optional display initials for this space's avatar. Supports a maximum of 2 characters. + * If initials are not provided, then they will be derived from the space name automatically. + * + * Initials are not displayed if an `imageUrl` has been specified. + */ + initials?: string; + + /** + * Optional base-64 encoded data image url to show as this space's avatar. + * This setting takes precedence over any configured `color` or `initials`. + */ + imageUrl?: string; + + /** + * The set of feature ids that should be hidden within this space. + */ + disabledFeatures: string[]; + + /** + * Indicates that this space is reserved (system controlled). + * Reserved spaces cannot be created or deleted by end-users. + * @private + */ + _reserved?: boolean; +} /** * Controls how spaces are retrieved. diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index 68ea4c18cfacc..1874fa0d41b27 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -8,7 +8,7 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "spaces"], - "requiredPlugins": ["features", "licensing", "spacesOss"], + "requiredPlugins": ["features", "licensing"], "optionalPlugins": [ "advancedSettings", "home", diff --git a/x-pack/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx b/x-pack/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx index 5658f95b62854..28d64e41fcbe4 100644 --- a/x-pack/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx +++ b/x-pack/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx @@ -8,8 +8,8 @@ import React from 'react'; import type { AdvancedSettingsSetup } from 'src/plugins/advanced_settings/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../common'; import { AdvancedSettingsSubtitle, AdvancedSettingsTitle } from './components'; interface SetupDeps { diff --git a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx b/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx index 75a27a3738e61..613cd9fdaebce 100644 --- a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx +++ b/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx @@ -9,7 +9,8 @@ import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import React, { Fragment, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Space } from 'src/plugins/spaces_oss/common'; + +import type { Space } from '../../../../common'; interface Props { getActiveSpace: () => Promise; diff --git a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx b/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx index 9bec9e32ca736..a5af84bd33948 100644 --- a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx +++ b/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx @@ -9,8 +9,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiTitle } from '@elastic import React, { lazy, Suspense, useEffect, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../../../common'; import { getSpaceAvatarComponent } from '../../../space_avatar'; // No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx index 5d68a5dd14844..f1472032fffa1 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import type { CopyToSpaceFlyoutProps } from 'src/plugins/spaces_oss/public'; +import type { CopyToSpaceFlyoutProps } from '../types'; export const getCopyToSpaceFlyoutComponent = async (): Promise< React.FC diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.test.tsx index 06d4b18311702..2bad5757613e0 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.test.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.test.tsx @@ -12,11 +12,11 @@ import React from 'react'; import { findTestSubject, mountWithIntl, nextTick } from '@kbn/test/jest'; import { coreMock } from 'src/core/public/mocks'; -import type { Space } from 'src/plugins/spaces_oss/common'; -import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; +import type { Space } from '../../../common'; import { getSpacesContextProviderWrapper } from '../../spaces_context'; import { spacesManagerMock } from '../../spaces_manager/mocks'; +import type { CopyToSpaceSavedObjectTarget } from '../types'; import { CopyModeControl } from './copy_mode_control'; import { getCopyToSpaceFlyoutComponent } from './copy_to_space_flyout'; import { CopyToSpaceForm } from './copy_to_space_form'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx index dc92abacae151..d82a5e79940c3 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx @@ -25,12 +25,11 @@ import React, { useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public'; -import type { CopyToSpaceFlyoutProps } from 'src/plugins/spaces_oss/public'; import { processImportResponse } from '../../../../../../src/plugins/saved_objects_management/public'; import { useSpaces } from '../../spaces_context'; import type { SpacesDataEntry } from '../../types'; -import type { CopyOptions, ImportRetry } from '../types'; +import type { CopyOptions, CopyToSpaceFlyoutProps, ImportRetry } from '../types'; import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer'; import { CopyToSpaceForm } from './copy_to_space_form'; import { ProcessingCopyToSpace } from './processing_copy_to_space'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx index e095becf68fbe..1b92936816e52 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx @@ -9,10 +9,9 @@ import { EuiFormRow, EuiSpacer, EuiTitle } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; import type { SpacesDataEntry } from '../../types'; -import type { CopyOptions } from '../types'; +import type { CopyOptions, CopyToSpaceSavedObjectTarget } from '../types'; import type { CopyMode } from './copy_mode_control'; import { CopyModeControl } from './copy_mode_control'; import { SelectableSpacesControl } from './selectable_spaces_control'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx index fe15ae16b252a..177d87d9f0f41 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx @@ -16,11 +16,10 @@ import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import type { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public'; -import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; import type { SpacesDataEntry } from '../../types'; import { summarizeCopyResult } from '../lib'; -import type { CopyOptions, ImportRetry } from '../types'; +import type { CopyOptions, CopyToSpaceSavedObjectTarget, ImportRetry } from '../types'; import { SpaceResult, SpaceResultProcessing } from './space_result'; interface Props { diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/index.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/index.ts index bbaa6a49f7a47..2443c8443c091 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/index.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/index.ts @@ -6,3 +6,4 @@ */ export { getCopyToSpaceFlyoutComponent } from './components'; +export type { CopyToSpaceFlyoutProps, CopyToSpaceSavedObjectTarget } from './types'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts index 07f18cda2c7fb..120a0bd8e7b1b 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts @@ -10,8 +10,8 @@ import type { ProcessedImportResponse, SavedObjectsManagementRecord, } from 'src/plugins/saved_objects_management/public'; -import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; +import type { CopyToSpaceSavedObjectTarget } from '../types'; import { summarizeCopyResult } from './summarize_copy_result'; // Sample data references: diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts index 07534a47cb860..9274053ce81ec 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts @@ -13,7 +13,8 @@ import type { FailedImport, ProcessedImportResponse, } from 'src/plugins/saved_objects_management/public'; -import type { CopyToSpaceSavedObjectTarget } from 'src/plugins/spaces_oss/public'; + +import type { CopyToSpaceSavedObjectTarget } from '../types'; export interface SummarizedSavedObjectResult { type: string; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts index 48ec1c6552a87..732d779c6f616 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/types.ts @@ -19,3 +19,47 @@ export type ImportRetry = Omit; export interface CopySavedObjectsToSpaceResponse { [spaceId: string]: SavedObjectsImportResponse; } + +/** + * Properties for the CopyToSpaceFlyout. + */ +export interface CopyToSpaceFlyoutProps { + /** + * The object to render the flyout for. + */ + savedObjectTarget: CopyToSpaceSavedObjectTarget; + /** + * Optional callback when the flyout is closed. + */ + onClose?: () => void; +} + +/** + * Describes the target saved object during a copy operation. + */ +export interface CopyToSpaceSavedObjectTarget { + /** + * The object's type. + */ + type: string; + /** + * The object's ID. + */ + id: string; + /** + * The namespaces that the object currently exists in. + */ + namespaces: string[]; + /** + * The EUI icon that is rendered in the flyout's subtitle. + * + * Default is 'apps'. + */ + icon?: string; + /** + * The string that is rendered in the flyout's subtitle. + * + * Default is `${type} [id=${id}]`. + */ + title?: string; +} diff --git a/x-pack/plugins/spaces/public/index.ts b/x-pack/plugins/spaces/public/index.ts index 1f3c2ba5b4c84..e7fa0e814b2ca 100644 --- a/x-pack/plugins/spaces/public/index.ts +++ b/x-pack/plugins/spaces/public/index.ts @@ -11,18 +11,28 @@ export { getSpaceColor, getSpaceImageUrl, getSpaceInitials } from './space_avata export { SpacesPluginSetup, SpacesPluginStart } from './plugin'; -export type { GetAllSpacesPurpose, GetSpaceResult } from '../common'; +export type { Space, GetAllSpacesPurpose, GetSpaceResult } from '../common'; + +export type { SpacesData, SpacesDataEntry, SpacesApi } from './types'; -// re-export types from oss definition -export type { Space } from 'src/plugins/spaces_oss/common'; export type { - SpacesApi, - SpacesApiUi, - SpacesContextProps, - ShareToSpaceFlyoutProps, CopyToSpaceFlyoutProps, - SpaceListProps, -} from 'src/plugins/spaces_oss/public'; + CopyToSpaceSavedObjectTarget, +} from './copy_saved_objects_to_space'; + +export type { + LegacyUrlConflictProps, + ShareToSpaceFlyoutProps, + ShareToSpaceSavedObjectTarget, +} from './share_saved_objects_to_space'; + +export type { SpaceAvatarProps } from './space_avatar'; + +export type { SpaceListProps } from './space_list'; + +export type { SpacesContextProps } from './spaces_context'; + +export type { LazyComponentFn, SpacesApiUi, SpacesApiUiComponent } from './ui_api'; export const plugin = () => { return new SpacesPlugin(); diff --git a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx index f3ed578a94962..4c808f9a47582 100644 --- a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx +++ b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/confirm_delete_modal.tsx @@ -13,9 +13,9 @@ import useAsyncFn from 'react-use/lib/useAsyncFn'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Space } from 'src/plugins/spaces_oss/common'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import type { Space } from '../../../../common'; import type { SpacesManager } from '../../../spaces_manager'; interface Props { diff --git a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx b/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx index 92b68426d172e..dc78441c3ba02 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/delete_spaces_button.tsx @@ -12,8 +12,8 @@ import React, { Component, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { NotificationsStart } from 'src/core/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../../common'; import type { SpacesManager } from '../../spaces_manager'; import { ConfirmDeleteModal } from '../components/confirm_delete_modal'; diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx index 7481676430307..9860d701ee4ab 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/enabled_features.tsx @@ -11,10 +11,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Space } from 'src/plugins/spaces_oss/common'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import type { KibanaFeatureConfig } from '../../../../../features/public'; +import type { Space } from '../../../../common'; import { SectionPanel } from '../section_panel'; import { FeatureTable } from './feature_table'; diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx index 78ea73741a8ad..2371a97370b53 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/feature_table.tsx @@ -25,9 +25,9 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import type { AppCategory } from 'src/core/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; import type { KibanaFeatureConfig } from '../../../../../features/public'; +import type { Space } from '../../../../common'; import { getEnabledFeatures } from '../../lib/feature_utils'; interface Props { diff --git a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx index 5a7ae701e97e0..bd0621e539ed0 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/manage_space_page.tsx @@ -23,10 +23,10 @@ import React, { Component } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { Capabilities, NotificationsStart, ScopedHistory } from 'src/core/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; import { SectionLoading } from '../../../../../../src/plugins/es_ui_shared/public'; import type { FeaturesPluginStart, KibanaFeature } from '../../../../features/public'; +import type { Space } from '../../../common'; import { isReservedSpace } from '../../../common'; import { getSpacesFeatureDescription } from '../../constants'; import { getSpaceColor, getSpaceInitials } from '../../space_avatar'; diff --git a/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx b/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx index da32ea79724ad..6415444a05620 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/reserved_space_badge.tsx @@ -9,8 +9,8 @@ import { EuiBadge, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../../common'; import { isReservedSpace } from '../../../common'; interface Props { diff --git a/x-pack/plugins/spaces/public/management/lib/feature_utils.ts b/x-pack/plugins/spaces/public/management/lib/feature_utils.ts index d332b3e34c0a6..7598e18d6680c 100644 --- a/x-pack/plugins/spaces/public/management/lib/feature_utils.ts +++ b/x-pack/plugins/spaces/public/management/lib/feature_utils.ts @@ -5,9 +5,8 @@ * 2.0. */ -import type { Space } from 'src/plugins/spaces_oss/common'; - import type { KibanaFeatureConfig } from '../../../../features/common'; +import type { Space } from '../../../common'; export function getEnabledFeatures(features: KibanaFeatureConfig[], space: Partial) { return features.filter((feature) => !(space.disabledFeatures || []).includes(feature.id)); diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index 200b868a1b103..e40f92bd54486 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -26,10 +26,10 @@ import type { NotificationsStart, ScopedHistory, } from 'src/core/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; import { reactRouterNavigate } from '../../../../../../src/plugins/kibana_react/public'; import type { FeaturesPluginStart, KibanaFeature } from '../../../../features/public'; +import type { Space } from '../../../common'; import { isReservedSpace } from '../../../common'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { getSpacesFeatureDescription } from '../../constants'; diff --git a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx index 4bf8b46fecb75..a7a7591b94562 100644 --- a/x-pack/plugins/spaces/public/management/spaces_management_app.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_management_app.tsx @@ -12,13 +12,13 @@ import { Route, Router, Switch, useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import type { StartServicesAccessor } from 'src/core/public'; import type { RegisterManagementAppArgs } from 'src/plugins/management/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; import { APP_WRAPPER_CLASS } from '../../../../../src/core/public'; import { KibanaContextProvider, RedirectAppLinks, } from '../../../../../src/plugins/kibana_react/public'; +import type { Space } from '../../common'; import type { PluginsStart } from '../plugin'; import type { SpacesManager } from '../spaces_manager'; diff --git a/src/plugins/spaces_oss/public/api.mock.ts b/x-pack/plugins/spaces/public/mocks.ts similarity index 72% rename from src/plugins/spaces_oss/public/api.mock.ts rename to x-pack/plugins/spaces/public/mocks.ts index 9706ae6b56944..31697c86b7f1e 100644 --- a/src/plugins/spaces_oss/public/api.mock.ts +++ b/x-pack/plugins/spaces/public/mocks.ts @@ -1,14 +1,15 @@ /* * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. */ import { of } from 'rxjs'; -import type { SpacesApi, SpacesApiUi, SpacesApiUiComponent } from './api'; +import type { SpacesPluginStart } from './plugin'; +import type { SpacesApi } from './types'; +import type { SpacesApiUi, SpacesApiUiComponent } from './ui_api'; const createApiMock = (): jest.Mocked => ({ getActiveSpace$: jest.fn().mockReturnValue(of()), @@ -44,6 +45,8 @@ const createApiUiComponentsMock = () => { return mock; }; -export const spacesApiMock = { - create: createApiMock, +const createStartContract = (): jest.Mocked => createApiMock(); + +export const spacesPluginMock = { + createStartContract, }; diff --git a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx index 392a95c445921..5fafe151dade9 100644 --- a/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx +++ b/x-pack/plugins/spaces/public/nav_control/components/spaces_menu.tsx @@ -21,8 +21,8 @@ import React, { Component, lazy, Suspense } from 'react'; import type { InjectedIntl } from '@kbn/i18n/react'; import { FormattedMessage, injectI18n } from '@kbn/i18n/react'; import type { ApplicationStart, Capabilities } from 'src/core/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../../common'; import { addSpaceIdToPath, ENTER_SPACE_PATH, SPACE_SEARCH_COUNT_THRESHOLD } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; import { ManageSpacesButton } from './manage_spaces_button'; diff --git a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx index 1653506c550f6..41a05a38fa305 100644 --- a/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx +++ b/x-pack/plugins/spaces/public/nav_control/nav_control_popover.tsx @@ -11,8 +11,8 @@ import React, { Component, lazy, Suspense } from 'react'; import type { Subscription } from 'rxjs'; import type { ApplicationStart, Capabilities } from 'src/core/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../common'; import { getSpaceAvatarComponent } from '../space_avatar'; import type { SpacesManager } from '../spaces_manager'; import { SpacesDescription } from './components/spaces_description'; diff --git a/x-pack/plugins/spaces/public/plugin.test.ts b/x-pack/plugins/spaces/public/plugin.test.ts index 39478ca2fd9be..43b701c48b2c2 100644 --- a/x-pack/plugins/spaces/public/plugin.test.ts +++ b/x-pack/plugins/spaces/public/plugin.test.ts @@ -12,7 +12,6 @@ import { createManagementSectionMock, managementPluginMock, } from 'src/plugins/management/public/mocks'; -import { spacesOssPluginMock } from 'src/plugins/spaces_oss/public/mocks'; import { SpacesPlugin } from './plugin'; @@ -20,12 +19,9 @@ describe('Spaces plugin', () => { describe('#setup', () => { it('should register the spaces API and the space selector app', () => { const coreSetup = coreMock.createSetup(); - const spacesOss = spacesOssPluginMock.createSetupContract(); const plugin = new SpacesPlugin(); - plugin.setup(coreSetup, { spacesOss }); - - expect(spacesOss.registerSpacesApi).toHaveBeenCalledTimes(1); + plugin.setup(coreSetup, {}); expect(coreSetup.application.register).toHaveBeenCalledWith( expect.objectContaining({ @@ -39,7 +35,6 @@ describe('Spaces plugin', () => { it('should register the management and feature catalogue sections when the management and home plugins are both available', () => { const coreSetup = coreMock.createSetup(); - const spacesOss = spacesOssPluginMock.createSetupContract(); const home = homePluginMock.createSetupContract(); const management = managementPluginMock.createSetupContract(); @@ -50,7 +45,6 @@ describe('Spaces plugin', () => { const plugin = new SpacesPlugin(); plugin.setup(coreSetup, { - spacesOss, management, home, }); @@ -71,11 +65,10 @@ describe('Spaces plugin', () => { it('should register the advanced settings components if the advanced_settings plugin is available', () => { const coreSetup = coreMock.createSetup(); - const spacesOss = spacesOssPluginMock.createSetupContract(); const advancedSettings = advancedSettingsMock.createSetupContract(); const plugin = new SpacesPlugin(); - plugin.setup(coreSetup, { spacesOss, advancedSettings }); + plugin.setup(coreSetup, { advancedSettings }); expect(advancedSettings.component.register.mock.calls).toMatchInlineSnapshot(` Array [ @@ -97,11 +90,10 @@ describe('Spaces plugin', () => { describe('#start', () => { it('should register the spaces nav control', () => { const coreSetup = coreMock.createSetup(); - const spacesOss = spacesOssPluginMock.createSetupContract(); const coreStart = coreMock.createStart(); const plugin = new SpacesPlugin(); - plugin.setup(coreSetup, { spacesOss }); + plugin.setup(coreSetup, {}); plugin.start(coreStart); diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 56e2f1143ae55..782cb3ba708a3 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -9,7 +9,6 @@ import type { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import type { AdvancedSettingsSetup } from 'src/plugins/advanced_settings/public'; import type { HomePublicPluginSetup } from 'src/plugins/home/public'; import type { ManagementSetup, ManagementStart } from 'src/plugins/management/public'; -import type { SpacesApi, SpacesOssPluginSetup } from 'src/plugins/spaces_oss/public'; import type { FeaturesPluginStart } from '../../features/public'; import { AdvancedSettingsService } from './advanced_settings'; @@ -18,10 +17,10 @@ import { ManagementService } from './management'; import { initSpacesNavControl } from './nav_control'; import { spaceSelectorApp } from './space_selector'; import { SpacesManager } from './spaces_manager'; +import type { SpacesApi } from './types'; import { getUiApi } from './ui_api'; export interface PluginsSetup { - spacesOss: SpacesOssPluginSetup; advancedSettings?: AdvancedSettingsSetup; home?: HomePublicPluginSetup; management?: ManagementSetup; @@ -86,8 +85,6 @@ export class SpacesPlugin implements Plugin diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx index f02cae7674058..8b435865c760f 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.test.tsx @@ -14,8 +14,8 @@ import React from 'react'; import { findTestSubject, mountWithIntl, nextTick } from '@kbn/test/jest'; import type { SavedObjectReferenceWithContext } from 'src/core/public'; import { coreMock } from 'src/core/public/mocks'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../../common'; import { ALL_SPACES_ID } from '../../../common/constants'; import { CopyToSpaceFlyoutInternal } from '../../copy_saved_objects_to_space/components/copy_to_space_flyout_internal'; import { getSpacesContextProviderWrapper } from '../../spaces_context'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx index 63dfedc497b94..076825e7c70aa 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_flyout_internal.tsx @@ -26,17 +26,17 @@ import React, { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { SavedObjectReferenceWithContext, ToastsStart } from 'src/core/public'; -import type { - ShareToSpaceFlyoutProps, - ShareToSpaceSavedObjectTarget, -} from 'src/plugins/spaces_oss/public'; import { ALL_SPACES_ID, UNKNOWN_SPACE } from '../../../common/constants'; import { getCopyToSpaceFlyoutComponent } from '../../copy_saved_objects_to_space'; import { useSpaces } from '../../spaces_context'; import type { SpacesManager } from '../../spaces_manager'; import type { SpacesDataEntry } from '../../types'; -import type { ShareOptions } from '../types'; +import type { + ShareOptions, + ShareToSpaceFlyoutProps, + ShareToSpaceSavedObjectTarget, +} from '../types'; import { AliasTable } from './alias_table'; import { DEFAULT_OBJECT_NOUN } from './constants'; import { RelativesFooter } from './relatives_footer'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/index.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/index.ts index d735f57ea6776..fe90ee8d6a8a9 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/index.ts +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/index.ts @@ -7,3 +7,8 @@ export { getShareToSpaceFlyoutComponent, getLegacyUrlConflict } from './components'; export { createRedirectLegacyUrl } from './utils'; +export type { + LegacyUrlConflictProps, + ShareToSpaceFlyoutProps, + ShareToSpaceSavedObjectTarget, +} from './types'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts index be8165e822736..1beccaa546282 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts @@ -17,3 +17,126 @@ export type ImportRetry = Omit; export interface ShareSavedObjectsToSpaceResponse { [spaceId: string]: SavedObjectsImportResponse; } + +/** + * Properties for the LegacyUrlConflict component. + */ +export interface LegacyUrlConflictProps { + /** + * The string that is used to describe the object in the callout, e.g., _There is a legacy URL for this page that points to a different + * **object**_. + * + * Default value is 'object'. + */ + objectNoun?: string; + /** + * The ID of the object that is currently shown on the page. + */ + currentObjectId: string; + /** + * The ID of the other object that the legacy URL alias points to. + */ + otherObjectId: string; + /** + * The path to use for the new URL, optionally including `search` and/or `hash` URL components. + */ + otherObjectPath: string; +} + +/** + * Properties for the ShareToSpaceFlyout. + */ +export interface ShareToSpaceFlyoutProps { + /** + * The object to render the flyout for. + */ + savedObjectTarget: ShareToSpaceSavedObjectTarget; + /** + * The EUI icon that is rendered in the flyout's title. + * + * Default is 'share'. + */ + flyoutIcon?: string; + /** + * The string that is rendered in the flyout's title. + * + * Default is 'Edit spaces for object'. + */ + flyoutTitle?: string; + /** + * When enabled, if the object is not yet shared to multiple spaces, a callout will be displayed that suggests the user might want to + * create a copy instead. + * + * Default value is false. + */ + enableCreateCopyCallout?: boolean; + /** + * When enabled, if no other spaces exist _and_ the user has the appropriate privileges, a sentence will be displayed that suggests the + * user might want to create a space. + * + * Default value is false. + */ + enableCreateNewSpaceLink?: boolean; + /** + * When set to 'within-space' (default), the flyout behaves like it is running on a page within the active space, and it will prevent the + * user from removing the object from the active space. + * + * Conversely, when set to 'outside-space', the flyout behaves like it is running on a page outside of any space, so it will allow the + * user to remove the object from the active space. + */ + behaviorContext?: 'within-space' | 'outside-space'; + /** + * Optional handler that is called when the user has saved changes and there are spaces to be added to and/or removed from the object and + * its relatives. If this is not defined, a default handler will be used that calls `/api/spaces/_update_objects_spaces` and displays a + * toast indicating what occurred. + */ + changeSpacesHandler?: ( + objects: Array<{ type: string; id: string }>, + spacesToAdd: string[], + spacesToRemove: string[] + ) => Promise; + /** + * Optional callback when the target object and its relatives are updated. + */ + onUpdate?: (updatedObjects: Array<{ type: string; id: string }>) => void; + /** + * Optional callback when the flyout is closed. + */ + onClose?: () => void; +} + +/** + * Describes the target saved object during a share operation. + */ +export interface ShareToSpaceSavedObjectTarget { + /** + * The object's type. + */ + type: string; + /** + * The object's ID. + */ + id: string; + /** + * The namespaces that the object currently exists in. + */ + namespaces: string[]; + /** + * The EUI icon that is rendered in the flyout's subtitle. + * + * Default is 'empty'. + */ + icon?: string; + /** + * The string that is rendered in the flyout's subtitle. + * + * Default is `${type} [id=${id}]`. + */ + title?: string; + /** + * The string that is used to describe the object in several places, e.g., _Make **object** available in selected spaces only_. + * + * Default value is 'object'. + */ + noun?: string; +} diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/utils/redirect_legacy_url.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/utils/redirect_legacy_url.ts index 338b2e8c94e0d..d427b1bc05242 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/utils/redirect_legacy_url.ts +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/utils/redirect_legacy_url.ts @@ -9,9 +9,9 @@ import { first } from 'rxjs/operators'; import { i18n } from '@kbn/i18n'; import type { StartServicesAccessor } from 'src/core/public'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; import type { PluginsStart } from '../../plugin'; +import type { SpacesApiUi } from '../../ui_api'; import { DEFAULT_OBJECT_NOUN } from '../components/constants'; export function createRedirectLegacyUrl( diff --git a/x-pack/plugins/spaces/public/space_avatar/index.ts b/x-pack/plugins/spaces/public/space_avatar/index.ts index 86d94738f2c79..a8d67b3964665 100644 --- a/x-pack/plugins/spaces/public/space_avatar/index.ts +++ b/x-pack/plugins/spaces/public/space_avatar/index.ts @@ -7,3 +7,4 @@ export { getSpaceAvatarComponent } from './space_avatar'; export * from './space_attributes'; +export type { SpaceAvatarProps } from './types'; diff --git a/x-pack/plugins/spaces/public/space_avatar/space_attributes.ts b/x-pack/plugins/spaces/public/space_avatar/space_attributes.ts index 682a61c6f23a5..047e544f94056 100644 --- a/x-pack/plugins/spaces/public/space_avatar/space_attributes.ts +++ b/x-pack/plugins/spaces/public/space_avatar/space_attributes.ts @@ -7,8 +7,7 @@ import { VISUALIZATION_COLORS } from '@elastic/eui'; -import type { Space } from 'src/plugins/spaces_oss/common'; - +import type { Space } from '../../common'; import { MAX_SPACE_INITIALS } from '../../common'; // code point for lowercase "a" diff --git a/x-pack/plugins/spaces/public/space_avatar/space_avatar.tsx b/x-pack/plugins/spaces/public/space_avatar/space_avatar.tsx index a2d58a12c5eaa..2bd86effcc656 100644 --- a/x-pack/plugins/spaces/public/space_avatar/space_avatar.tsx +++ b/x-pack/plugins/spaces/public/space_avatar/space_avatar.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import type { SpaceAvatarProps } from 'src/plugins/spaces_oss/public'; +import type { SpaceAvatarProps } from './types'; export const getSpaceAvatarComponent = async (): Promise> => { const { SpaceAvatarInternal } = await import('./space_avatar_internal'); diff --git a/x-pack/plugins/spaces/public/space_avatar/space_avatar_internal.tsx b/x-pack/plugins/spaces/public/space_avatar/space_avatar_internal.tsx index 91b4dbf8a964e..b8fd5ed77f488 100644 --- a/x-pack/plugins/spaces/public/space_avatar/space_avatar_internal.tsx +++ b/x-pack/plugins/spaces/public/space_avatar/space_avatar_internal.tsx @@ -10,25 +10,11 @@ import { EuiAvatar, isValidHex } from '@elastic/eui'; import type { FC } from 'react'; import React from 'react'; -import type { Space } from 'src/plugins/spaces_oss/common'; - import { MAX_SPACE_INITIALS } from '../../common'; import { getSpaceColor, getSpaceImageUrl, getSpaceInitials } from './space_attributes'; +import type { SpaceAvatarProps } from './types'; -interface Props { - space: Partial; - size?: 's' | 'm' | 'l' | 'xl'; - className?: string; - announceSpaceName?: boolean; - /** - * This property is passed to the underlying `EuiAvatar` component. If enabled, the SpaceAvatar will have a grayed out appearance. For - * example, this can be useful when rendering a list of spaces for a specific feature, if the feature is disabled in one of those spaces. - * Default: false. - */ - isDisabled?: boolean; -} - -export const SpaceAvatarInternal: FC = (props: Props) => { +export const SpaceAvatarInternal: FC = (props: SpaceAvatarProps) => { const { space, size, announceSpaceName, ...rest } = props; const spaceName = space.name ? space.name.trim() : ''; diff --git a/x-pack/plugins/spaces/public/space_avatar/types.ts b/x-pack/plugins/spaces/public/space_avatar/types.ts new file mode 100644 index 0000000000000..365c71eeeea73 --- /dev/null +++ b/x-pack/plugins/spaces/public/space_avatar/types.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Space } from '../../common'; + +/** + * Properties for the SpaceAvatar component. + */ +export interface SpaceAvatarProps { + /** The space to represent with an avatar. */ + space: Partial; + + /** The size of the avatar. */ + size?: 's' | 'm' | 'l' | 'xl'; + + /** Optional CSS class(es) to apply. */ + className?: string; + + /** + * When enabled, allows EUI to provide an aria-label for this component, which is announced on screen readers. + * + * Default value is true. + */ + announceSpaceName?: boolean; + + /** + * Whether or not to render the avatar in a disabled state. + * + * Default value is false. + */ + isDisabled?: boolean; +} diff --git a/x-pack/plugins/spaces/public/space_list/index.ts b/x-pack/plugins/spaces/public/space_list/index.ts index 1570ad123b9ab..9d367f3739c70 100644 --- a/x-pack/plugins/spaces/public/space_list/index.ts +++ b/x-pack/plugins/spaces/public/space_list/index.ts @@ -6,3 +6,4 @@ */ export { getSpaceListComponent } from './space_list'; +export type { SpaceListProps } from './types'; diff --git a/x-pack/plugins/spaces/public/space_list/space_list.tsx b/x-pack/plugins/spaces/public/space_list/space_list.tsx index efd8b367bcd45..86e6432d1e210 100644 --- a/x-pack/plugins/spaces/public/space_list/space_list.tsx +++ b/x-pack/plugins/spaces/public/space_list/space_list.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import type { SpaceListProps } from 'src/plugins/spaces_oss/public'; +import type { SpaceListProps } from './types'; export const getSpaceListComponent = async (): Promise> => { const { SpaceListInternal } = await import('./space_list_internal'); diff --git a/x-pack/plugins/spaces/public/space_list/space_list_internal.test.tsx b/x-pack/plugins/spaces/public/space_list/space_list_internal.test.tsx index 8109444fc1271..39ae339fb7e18 100644 --- a/x-pack/plugins/spaces/public/space_list/space_list_internal.test.tsx +++ b/x-pack/plugins/spaces/public/space_list/space_list_internal.test.tsx @@ -11,12 +11,12 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { coreMock } from 'src/core/public/mocks'; -import type { Space } from 'src/plugins/spaces_oss/common'; -import type { SpaceListProps } from 'src/plugins/spaces_oss/public'; +import type { Space } from '../../common'; import { getSpacesContextProviderWrapper } from '../spaces_context'; import { spacesManagerMock } from '../spaces_manager/mocks'; import { SpaceListInternal } from './space_list_internal'; +import type { SpaceListProps } from './types'; const ACTIVE_SPACE: Space = { id: 'default', diff --git a/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx b/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx index 6c612db9f759d..bfe4486fafa76 100644 --- a/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx +++ b/x-pack/plugins/spaces/public/space_list/space_list_internal.tsx @@ -18,12 +18,12 @@ import React, { lazy, Suspense, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { SpaceListProps } from 'src/plugins/spaces_oss/public'; import { ALL_SPACES_ID, UNKNOWN_SPACE } from '../../common/constants'; import { getSpaceAvatarComponent } from '../space_avatar'; import { useSpaces } from '../spaces_context'; import type { SpacesData, SpacesDataEntry } from '../types'; +import type { SpaceListProps } from './types'; // No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. const LazySpaceAvatar = lazy(() => diff --git a/x-pack/plugins/spaces/public/space_list/types.ts b/x-pack/plugins/spaces/public/space_list/types.ts new file mode 100644 index 0000000000000..2e7e813a48a2f --- /dev/null +++ b/x-pack/plugins/spaces/public/space_list/types.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Properties for the SpaceList component. + */ +export interface SpaceListProps { + /** + * The namespaces of a saved object to render into a corresponding list of spaces. + */ + namespaces: string[]; + /** + * Optional limit to the number of spaces that can be displayed in the list. If the number of spaces exceeds this limit, they will be + * hidden behind a "show more" button. Set to 0 to disable. + * + * Default value is 5. + */ + displayLimit?: number; + /** + * When set to 'within-space' (default), the space list behaves like it is running on a page within the active space, and it will omit the + * active space (e.g., it displays a list of all the _other_ spaces that an object is shared to). + * + * Conversely, when set to 'outside-space', the space list behaves like it is running on a page outside of any space, so it will not omit + * the active space. + */ + behaviorContext?: 'within-space' | 'outside-space'; +} diff --git a/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx b/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx index 0628f79990af6..214659169e72d 100644 --- a/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx +++ b/x-pack/plugins/spaces/public/space_selector/components/space_card.tsx @@ -10,8 +10,7 @@ import './space_card.scss'; import { EuiCard, EuiLoadingSpinner } from '@elastic/eui'; import React, { lazy, Suspense } from 'react'; -import type { Space } from 'src/plugins/spaces_oss/common'; - +import type { Space } from '../../../common'; import { addSpaceIdToPath, ENTER_SPACE_PATH } from '../../../common'; import { getSpaceAvatarComponent } from '../../space_avatar'; diff --git a/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx b/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx index e7bef5f646036..c13a3a53fbe8f 100644 --- a/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx +++ b/x-pack/plugins/spaces/public/space_selector/components/space_cards.tsx @@ -10,8 +10,7 @@ import './space_cards.scss'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { Component } from 'react'; -import type { Space } from 'src/plugins/spaces_oss/common'; - +import type { Space } from '../../../common'; import { SpaceCard } from './space_card'; interface Props { diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx b/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx index b4417d98bcace..e17c3e0078d42 100644 --- a/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx +++ b/x-pack/plugins/spaces/public/space_selector/space_selector.test.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { shallowWithIntl } from '@kbn/test/jest'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../common'; import { spacesManagerMock } from '../spaces_manager/mocks'; import { SpaceSelector } from './space_selector'; diff --git a/x-pack/plugins/spaces/public/space_selector/space_selector.tsx b/x-pack/plugins/spaces/public/space_selector/space_selector.tsx index cee304408495d..00ad39bf0027f 100644 --- a/x-pack/plugins/spaces/public/space_selector/space_selector.tsx +++ b/x-pack/plugins/spaces/public/space_selector/space_selector.tsx @@ -28,8 +28,8 @@ import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import type { CoreStart } from 'src/core/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../common'; import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../common/constants'; import type { SpacesManager } from '../spaces_manager'; import { SpaceCards } from './components'; diff --git a/x-pack/plugins/spaces/public/spaces_context/index.ts b/x-pack/plugins/spaces/public/spaces_context/index.ts index 0187131b02b93..710f59d6b16d5 100644 --- a/x-pack/plugins/spaces/public/spaces_context/index.ts +++ b/x-pack/plugins/spaces/public/spaces_context/index.ts @@ -6,4 +6,5 @@ */ export { useSpaces } from './context'; +export type { SpacesContextProps } from './types'; export { getSpacesContextProviderWrapper } from './wrapper'; diff --git a/x-pack/plugins/spaces/public/spaces_context/types.ts b/x-pack/plugins/spaces/public/spaces_context/types.ts index 646bf60ea3012..01057c1550f0c 100644 --- a/x-pack/plugins/spaces/public/spaces_context/types.ts +++ b/x-pack/plugins/spaces/public/spaces_context/types.ts @@ -31,3 +31,13 @@ export interface InternalProps { spacesManager: SpacesManager; getStartServices: StartServicesAccessor; } + +/** + * Properties for the SpacesContext. + */ +export interface SpacesContextProps { + /** + * If a feature is specified, all Spaces components will treat it appropriately if the feature is disabled in a given Space. + */ + feature?: string; +} diff --git a/x-pack/plugins/spaces/public/spaces_context/wrapper.tsx b/x-pack/plugins/spaces/public/spaces_context/wrapper.tsx index 6de14290abb74..8fae6e78d1bb2 100644 --- a/x-pack/plugins/spaces/public/spaces_context/wrapper.tsx +++ b/x-pack/plugins/spaces/public/spaces_context/wrapper.tsx @@ -8,9 +8,7 @@ import type { PropsWithChildren } from 'react'; import React from 'react'; -import type { SpacesContextProps } from 'src/plugins/spaces_oss/public'; - -import type { InternalProps } from './types'; +import type { InternalProps, SpacesContextProps } from './types'; export const getSpacesContextProviderWrapper = async ( internalProps: InternalProps diff --git a/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx b/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx index a9ab68c864393..83ad69b1017d5 100644 --- a/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx +++ b/x-pack/plugins/spaces/public/spaces_context/wrapper_internal.tsx @@ -9,13 +9,12 @@ import type { PropsWithChildren } from 'react'; import React, { useEffect, useMemo, useState } from 'react'; import type { ApplicationStart, DocLinksStart, NotificationsStart } from 'src/core/public'; -import type { SpacesContextProps } from 'src/plugins/spaces_oss/public'; import type { GetAllSpacesPurpose } from '../../common'; import type { SpacesManager } from '../spaces_manager'; import type { SpacesData, SpacesDataEntry } from '../types'; import { createSpacesReactContext } from './context'; -import type { InternalProps, SpacesReactContext } from './types'; +import type { InternalProps, SpacesContextProps, SpacesReactContext } from './types'; interface Services { application: ApplicationStart; diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts index 5282163f93b15..3f3cc3f4d7801 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.mock.ts @@ -8,8 +8,7 @@ import type { Observable } from 'rxjs'; import { of } from 'rxjs'; -import type { Space } from 'src/plugins/spaces_oss/common'; - +import type { Space } from '../../common'; import type { SpacesManager } from './spaces_manager'; function createSpacesManagerMock() { diff --git a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts index 845373bf22299..d0406d744b72a 100644 --- a/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts +++ b/x-pack/plugins/spaces/public/spaces_manager/spaces_manager.ts @@ -13,9 +13,13 @@ import type { HttpSetup, SavedObjectsCollectMultiNamespaceReferencesResponse, } from 'src/core/public'; -import type { Space } from 'src/plugins/spaces_oss/common'; -import type { GetAllSpacesOptions, GetSpaceResult, LegacyUrlAliasTarget } from '../../common'; +import type { + GetAllSpacesOptions, + GetSpaceResult, + LegacyUrlAliasTarget, + Space, +} from '../../common'; import type { CopySavedObjectsToSpaceResponse } from '../copy_saved_objects_to_space/types'; interface SavedObjectTarget { diff --git a/x-pack/plugins/spaces/public/types.ts b/x-pack/plugins/spaces/public/types.ts index 3d9eab1c9e441..fd926621b72da 100644 --- a/x-pack/plugins/spaces/public/types.ts +++ b/x-pack/plugins/spaces/public/types.ts @@ -5,7 +5,10 @@ * 2.0. */ -import type { GetAllSpacesPurpose, GetSpaceResult } from '../common'; +import type { Observable } from 'rxjs'; + +import type { GetAllSpacesPurpose, GetSpaceResult, Space } from '../common'; +import type { SpacesApiUi } from './ui_api'; /** * The structure for all of the space data that must be loaded for share-to-space components to function. @@ -30,3 +33,24 @@ export interface SpacesDataEntry /** Returns true if the user is authorized for the given purpose. */ isAuthorizedForPurpose(purpose: GetAllSpacesPurpose): boolean; } + +/** + * Client-side Spaces API. + */ +export interface SpacesApi { + /** + * Observable representing the currently active space. + * The details of the space can change without a full page reload (such as display name, color, etc.) + */ + getActiveSpace$(): Observable; + + /** + * Retrieve the currently active space. + */ + getActiveSpace(): Promise; + + /** + * UI components and services to add spaces capabilities to an application. + */ + ui: SpacesApiUi; +} diff --git a/x-pack/plugins/spaces/public/ui_api/components.tsx b/x-pack/plugins/spaces/public/ui_api/components.tsx index ef69e0a05ed3f..a33480712ffae 100644 --- a/x-pack/plugins/spaces/public/ui_api/components.tsx +++ b/x-pack/plugins/spaces/public/ui_api/components.tsx @@ -9,7 +9,6 @@ import type { FC, PropsWithChildren, PropsWithRef } from 'react'; import React from 'react'; import type { StartServicesAccessor } from 'src/core/public'; -import type { SpacesApiUiComponent } from 'src/plugins/spaces_oss/public'; import { getCopyToSpaceFlyoutComponent } from '../copy_saved_objects_to_space'; import type { PluginsStart } from '../plugin'; @@ -22,6 +21,7 @@ import { getSpaceListComponent } from '../space_list'; import { getSpacesContextProviderWrapper } from '../spaces_context'; import type { SpacesManager } from '../spaces_manager'; import { LazyWrapper } from './lazy_wrapper'; +import type { SpacesApiUiComponent } from './types'; export interface GetComponentsOptions { spacesManager: SpacesManager; diff --git a/x-pack/plugins/spaces/public/ui_api/index.ts b/x-pack/plugins/spaces/public/ui_api/index.ts index 4bfb482b41407..0a88f772a76e8 100644 --- a/x-pack/plugins/spaces/public/ui_api/index.ts +++ b/x-pack/plugins/spaces/public/ui_api/index.ts @@ -6,18 +6,20 @@ */ import type { StartServicesAccessor } from 'src/core/public'; -import type { SpacesApiUi } from 'src/plugins/spaces_oss/public'; import type { PluginsStart } from '../plugin'; import { createRedirectLegacyUrl } from '../share_saved_objects_to_space'; import type { SpacesManager } from '../spaces_manager'; import { getComponents } from './components'; +import type { LazyComponentFn, SpacesApiUi, SpacesApiUiComponent } from './types'; interface GetUiApiOptions { spacesManager: SpacesManager; getStartServices: StartServicesAccessor; } +export type { LazyComponentFn, SpacesApiUi, SpacesApiUiComponent }; + export const getUiApi = ({ spacesManager, getStartServices }: GetUiApiOptions): SpacesApiUi => { const components = getComponents({ spacesManager, getStartServices }); diff --git a/x-pack/plugins/spaces/public/ui_api/mocks.ts b/x-pack/plugins/spaces/public/ui_api/mocks.ts deleted file mode 100644 index 3e5430c679f2a..0000000000000 --- a/x-pack/plugins/spaces/public/ui_api/mocks.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { SpacesApiUi, SpacesApiUiComponent } from 'src/plugins/spaces_oss/public'; - -function createComponentsMock(): jest.Mocked { - return { - getSpacesContextProvider: jest.fn(), - getShareToSpaceFlyout: jest.fn(), - getCopyToSpaceFlyout: jest.fn(), - getSpaceList: jest.fn(), - getLegacyUrlConflict: jest.fn(), - getSpaceAvatar: jest.fn(), - }; -} - -function createUiApiMock(): jest.Mocked { - return { - components: createComponentsMock(), - redirectLegacyUrl: jest.fn(), - }; -} - -export const uiApiMock = { - create: createUiApiMock, -}; diff --git a/x-pack/plugins/spaces/public/ui_api/types.ts b/x-pack/plugins/spaces/public/ui_api/types.ts new file mode 100644 index 0000000000000..5371b2b9ae8e2 --- /dev/null +++ b/x-pack/plugins/spaces/public/ui_api/types.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ReactElement } from 'react'; + +import type { CopyToSpaceFlyoutProps } from '../copy_saved_objects_to_space'; +import type { + LegacyUrlConflictProps, + ShareToSpaceFlyoutProps, +} from '../share_saved_objects_to_space'; +import type { SpaceAvatarProps } from '../space_avatar'; +import type { SpaceListProps } from '../space_list'; +import type { SpacesContextProps } from '../spaces_context'; + +/** + * Function that returns a promise for a lazy-loadable component. + */ +export type LazyComponentFn = (props: T) => ReactElement; + +/** + * UI components and services to add spaces capabilities to an application. + */ +export interface SpacesApiUi { + /** + * Lazy-loadable {@link SpacesApiUiComponent | React components} to support the Spaces feature. + */ + components: SpacesApiUiComponent; + /** + * Redirect the user from a legacy URL to a new URL. This needs to be used if a call to `SavedObjectsClient.resolve()` results in an + * `"aliasMatch"` outcome, which indicates that the user has loaded the page using a legacy URL. Calling this function will trigger a + * client-side redirect to the new URL, and it will display a toast to the user. + * + * Consumers need to determine the local path for the new URL on their own, based on the object ID that was used to call + * `SavedObjectsClient.resolve()` (old ID) and the object ID in the result (new ID). For example... + * + * The old object ID is `workpad-123` and the new object ID is `workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e`. + * + * Full legacy URL: `https://localhost:5601/app/canvas#/workpad/workpad-123/page/1` + * + * New URL path: `#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1` + * + * The protocol, hostname, port, base path, and app path are automatically included. + * + * @param path The path to use for the new URL, optionally including `search` and/or `hash` URL components. + * @param objectNoun The string that is used to describe the object in the toast, e.g., _The **object** you're looking for has a new + * location_. Default value is 'object'. + */ + redirectLegacyUrl: (path: string, objectNoun?: string) => Promise; +} + +/** + * React UI components to be used to display the Spaces feature in any application. + */ +export interface SpacesApiUiComponent { + /** + * Provides a context that is required to render some Spaces components. + */ + getSpacesContextProvider: LazyComponentFn; + /** + * Displays a flyout to edit the spaces that an object is shared to. + * + * Note: must be rendered inside of a SpacesContext. + */ + getShareToSpaceFlyout: LazyComponentFn; + /** + * Displays a flyout to copy an object to other spaces. + * + * Note: must be rendered inside of a SpacesContext. + */ + getCopyToSpaceFlyout: LazyComponentFn; + /** + * Displays a corresponding list of spaces for a given list of saved object namespaces. It shows up to five spaces (and an indicator for + * any number of spaces that the user is not authorized to see) by default. If more than five named spaces would be displayed, the extras + * (along with the unauthorized spaces indicator, if present) are hidden behind a button. If '*' (aka "All spaces") is present, it + * supersedes all of the above and just displays a single badge without a button. + * + * Note: must be rendered inside of a SpacesContext. + */ + getSpaceList: LazyComponentFn; + /** + * Displays a callout that needs to be used if a call to `SavedObjectsClient.resolve()` results in an `"conflict"` outcome, which + * indicates that the user has loaded the page which is associated directly with one object (A), *and* with a legacy URL that points to a + * different object (B). + * + * In this case, `SavedObjectsClient.resolve()` has returned object A. This component displays a warning callout to the user explaining + * that there is a conflict, and it includes a button that will redirect the user to object B when clicked. + * + * Consumers need to determine the local path for the new URL on their own, based on the object ID that was used to call + * `SavedObjectsClient.resolve()` (A) and the `alias_target_id` value in the response (B). For example... + * + * A is `workpad-123` and B is `workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e`. + * + * Full legacy URL: `https://localhost:5601/app/canvas#/workpad/workpad-123/page/1` + * + * New URL path: `#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1` + */ + getLegacyUrlConflict: LazyComponentFn; + /** + * Displays an avatar for the given space. + */ + getSpaceAvatar: LazyComponentFn; +} diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index c3393da770ded..13be8398da480 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -7,10 +7,10 @@ import type { Capabilities, CoreSetup } from 'src/core/server'; import { coreMock, httpServerMock, loggingSystemMock } from 'src/core/server/mocks'; -import type { Space } from 'src/plugins/spaces_oss/common'; import type { KibanaFeature } from '../../../features/server'; import { featuresPluginMock } from '../../../features/server/mocks'; +import type { Space } from '../../common'; import type { PluginsStart } from '../plugin'; import { spacesServiceMock } from '../spaces_service/spaces_service.mock'; import { setupCapabilitiesSwitcher } from './capabilities_switcher'; diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index 1f6249d9b3220..ac2ff735de797 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -8,9 +8,9 @@ import _ from 'lodash'; import type { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server'; -import type { Space } from 'src/plugins/spaces_oss/common'; import type { KibanaFeature } from '../../../features/server'; +import type { Space } from '../../common'; import type { PluginsStart } from '../plugin'; import type { SpacesServiceStart } from '../spaces_service'; diff --git a/x-pack/plugins/spaces/server/index.ts b/x-pack/plugins/spaces/server/index.ts index 4765b06f5a02a..31714df958d49 100644 --- a/x-pack/plugins/spaces/server/index.ts +++ b/x-pack/plugins/spaces/server/index.ts @@ -23,16 +23,14 @@ export { SpacesPluginSetup, SpacesPluginStart } from './plugin'; export { SpacesServiceSetup, SpacesServiceStart } from './spaces_service'; export { ISpacesClient, SpacesClientRepositoryFactory, SpacesClientWrapper } from './spaces_client'; -export { +export type { + Space, GetAllSpacesOptions, GetAllSpacesPurpose, GetSpaceResult, LegacyUrlAliasTarget, } from '../common'; -// re-export types from oss definition -export type { Space } from 'src/plugins/spaces_oss/common'; - export const config: PluginConfigDescriptor = { schema: ConfigSchema, deprecations: spacesConfigDeprecationProvider, diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index ff81960185b62..f5b41786d2478 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -6,8 +6,8 @@ */ import type { CoreSetup, Logger } from 'src/core/server'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../../common'; import { addSpaceIdToPath } from '../../../common'; import { DEFAULT_SPACE_ID, ENTER_SPACE_PATH } from '../../../common/constants'; import type { PluginsSetup } from '../../plugin'; diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts index 7f0bd3231c6dd..09a4ac5a843a6 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts @@ -6,8 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../../../common'; import { wrapError } from '../../../lib/errors'; import { createLicensedRouteHandler } from '../../lib'; import type { ExternalRouteDeps } from './'; diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.ts b/x-pack/plugins/spaces/server/routes/api/external/put.ts index b58601c8c7e77..196f7c11932eb 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.ts @@ -6,9 +6,9 @@ */ import { schema } from '@kbn/config-schema'; -import type { Space } from 'src/plugins/spaces_oss/common'; import { SavedObjectsErrorHelpers } from '../../../../../../../src/core/server'; +import type { Space } from '../../../../common'; import { wrapError } from '../../../lib/errors'; import { spaceSchema } from '../../../lib/space_schema'; import { createLicensedRouteHandler } from '../../lib'; diff --git a/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts b/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts index 6a90d367fc4a7..53969d3984bda 100644 --- a/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/lib/convert_saved_object_to_space.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../../common'; export function convertSavedObjectToSpace(savedObject: any): Space { return { diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.test.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.test.ts index 62a3d98662939..e07050fe97d73 100644 --- a/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.test.ts +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.test.ts @@ -5,8 +5,7 @@ * 2.0. */ -import type { Space } from 'src/plugins/spaces_oss/common'; - +import type { Space } from '../../../common'; import { migrateTo660 } from './space_migrations'; describe('migrateTo660', () => { diff --git a/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.ts b/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.ts index d88db2b0181dd..c26983e84de57 100644 --- a/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.ts +++ b/x-pack/plugins/spaces/server/saved_objects/migrations/space_migrations.ts @@ -6,7 +6,8 @@ */ import type { SavedObjectUnsanitizedDoc } from 'src/core/server'; -import type { Space } from 'src/plugins/spaces_oss/common'; + +import type { Space } from '../../../common'; export const migrateTo660 = (doc: SavedObjectUnsanitizedDoc) => { if (!doc.attributes.hasOwnProperty('disabledFeatures')) { diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client.mock.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.mock.ts index d893d2b089f89..a4e209ff11d32 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client.mock.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.mock.ts @@ -5,8 +5,7 @@ * 2.0. */ -import type { Space } from 'src/plugins/spaces_oss/common'; - +import type { Space } from '../../common'; import { DEFAULT_SPACE_ID } from '../../common/constants'; import type { SpacesClient } from './spaces_client'; diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts index 824d6e28b9923..0a91c7aff1a08 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts @@ -9,13 +9,13 @@ import Boom from '@hapi/boom'; import { omit } from 'lodash'; import type { ISavedObjectsRepository, SavedObject } from 'src/core/server'; -import type { Space } from 'src/plugins/spaces_oss/common'; import type { GetAllSpacesOptions, GetAllSpacesPurpose, GetSpaceResult, LegacyUrlAliasTarget, + Space, } from '../../common'; import { isReservedSpace } from '../../common'; import type { ConfigType } from '../config'; diff --git a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts index e951ed38072d7..2ffe77a4c9a89 100644 --- a/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts +++ b/x-pack/plugins/spaces/server/spaces_service/spaces_service.ts @@ -6,8 +6,8 @@ */ import type { IBasePath, KibanaRequest } from 'src/core/server'; -import type { Space } from 'src/plugins/spaces_oss/common'; +import type { Space } from '../../common'; import { getSpaceIdFromPath } from '../../common'; import { DEFAULT_SPACE_ID } from '../../common/constants'; import { namespaceToSpaceId, spaceIdToNamespace } from '../lib/utils/namespace'; diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index 4cc95504a158e..b55058d59b3fd 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -16,7 +16,6 @@ { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/management/tsconfig.json" }, { "path": "../../../src/plugins/saved_objects_management/tsconfig.json" }, - { "path": "../../../src/plugins/spaces_oss/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" } ] } From 13574cbd04723537fae286a060678e24dac39a52 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 18 Aug 2021 15:08:52 -0400 Subject: [PATCH 06/28] Remove spaces dependency on savedObjectsManagement --- x-pack/plugins/spaces/kibana.json | 1 - .../copy_to_space_flyout_footer.tsx | 9 ++-- .../copy_to_space_flyout_internal.tsx | 4 +- .../components/processing_copy_to_space.tsx | 2 +- .../copy_saved_objects_to_space/lib/index.ts | 3 ++ .../lib/process_import_response.ts | 46 +++++++++++++++++++ .../lib/summarize_copy_result.test.ts | 17 +++---- .../lib/summarize_copy_result.ts | 5 +- x-pack/plugins/spaces/tsconfig.json | 1 - 9 files changed, 62 insertions(+), 26 deletions(-) create mode 100644 x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/process_import_response.ts diff --git a/x-pack/plugins/spaces/kibana.json b/x-pack/plugins/spaces/kibana.json index 1874fa0d41b27..090e5d0894480 100644 --- a/x-pack/plugins/spaces/kibana.json +++ b/x-pack/plugins/spaces/kibana.json @@ -21,7 +21,6 @@ "requiredBundles": [ "esUiShared", "kibanaReact", - "savedObjectsManagement", "home" ] } diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx index c021d8bdf69a1..998b202a8d6b3 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_footer.tsx @@ -17,11 +17,8 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { - FailedImport, - ProcessedImportResponse, -} from 'src/plugins/saved_objects_management/public'; +import type { FailedImport, ProcessedImportResponse } from '../lib'; import type { ImportRetry } from '../types'; interface Props { @@ -62,8 +59,8 @@ export const CopyToSpaceFlyoutFooter = (props: Props) => { let pendingCount = 0; let skippedCount = 0; let errorCount = 0; - if (spaceResult.status === 'success') { - successCount = spaceResult.importCount; + if (spaceResult.success === true) { + successCount = spaceResult.successfulImports.length; } else { const uniqueResolvableErrors = spaceResult.failedImports .filter(isResolvableError) diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx index d82a5e79940c3..7697780c352c9 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout_internal.tsx @@ -24,11 +24,11 @@ import React, { useEffect, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public'; -import { processImportResponse } from '../../../../../../src/plugins/saved_objects_management/public'; import { useSpaces } from '../../spaces_context'; import type { SpacesDataEntry } from '../../types'; +import { processImportResponse } from '../lib'; +import type { ProcessedImportResponse } from '../lib'; import type { CopyOptions, CopyToSpaceFlyoutProps, ImportRetry } from '../types'; import { CopyToSpaceFlyoutFooter } from './copy_to_space_flyout_footer'; import { CopyToSpaceForm } from './copy_to_space_form'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx index 177d87d9f0f41..91ee2acf6bb42 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx @@ -15,9 +15,9 @@ import { import React, { Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import type { ProcessedImportResponse } from 'src/plugins/saved_objects_management/public'; import type { SpacesDataEntry } from '../../types'; +import type { ProcessedImportResponse } from '../lib'; import { summarizeCopyResult } from '../lib'; import type { CopyOptions, CopyToSpaceSavedObjectTarget, ImportRetry } from '../types'; import { SpaceResult, SpaceResultProcessing } from './space_result'; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/index.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/index.ts index 882ee234ca5dc..70a5cadd527bc 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/index.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/index.ts @@ -5,6 +5,9 @@ * 2.0. */ +export type { FailedImport, ProcessedImportResponse } from './process_import_response'; +export { processImportResponse } from './process_import_response'; + export type { SummarizedCopyToSpaceResult, SummarizedSavedObjectResult, diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/process_import_response.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/process_import_response.ts new file mode 100644 index 0000000000000..cf402b0dd4ec2 --- /dev/null +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/process_import_response.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + SavedObjectsImportAmbiguousConflictError, + SavedObjectsImportConflictError, + SavedObjectsImportFailure, + SavedObjectsImportMissingReferencesError, + SavedObjectsImportResponse, + SavedObjectsImportSuccess, + SavedObjectsImportUnknownError, + SavedObjectsImportUnsupportedTypeError, +} from 'src/core/public'; + +export interface FailedImport { + obj: Omit; + error: + | SavedObjectsImportConflictError + | SavedObjectsImportAmbiguousConflictError + | SavedObjectsImportUnsupportedTypeError + | SavedObjectsImportMissingReferencesError + | SavedObjectsImportUnknownError; +} + +export interface ProcessedImportResponse { + success: boolean; + failedImports: FailedImport[]; + successfulImports: SavedObjectsImportSuccess[]; +} + +// This is derived from the function of the same name in the savedObjectsManagement plugin +export function processImportResponse( + response: SavedObjectsImportResponse +): ProcessedImportResponse { + const { success, errors = [], successResults = [] } = response; + const failedImports = errors.map(({ error, ...obj }) => ({ obj, error })); + return { + success, + failedImports, + successfulImports: successResults, + }; +} diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts index 120a0bd8e7b1b..298b89050b637 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.test.ts @@ -5,12 +5,7 @@ * 2.0. */ -import type { - FailedImport, - ProcessedImportResponse, - SavedObjectsManagementRecord, -} from 'src/plugins/saved_objects_management/public'; - +import type { FailedImport, ProcessedImportResponse } from '../lib'; import type { CopyToSpaceSavedObjectTarget } from '../types'; import { summarizeCopyResult } from './summarize_copy_result'; @@ -43,7 +38,7 @@ const OBJECTS = { { type: 'visualization', id: 'foo', name: 'Visualization foo' }, { type: 'visualization', id: 'bar', name: 'Visualization bar' }, ], - } as SavedObjectsManagementRecord, + }, VISUALIZATION_FOO: { type: 'visualization', id: 'bar', @@ -54,7 +49,7 @@ const OBJECTS = { hiddenType: false, }, references: [{ type: 'index-pattern', id: 'foo', name: 'Index pattern foo' }], - } as SavedObjectsManagementRecord, + }, VISUALIZATION_BAR: { type: 'visualization', id: 'baz', @@ -65,7 +60,7 @@ const OBJECTS = { hiddenType: false, }, references: [{ type: 'index-pattern', id: 'bar', name: 'Index pattern bar' }], - } as SavedObjectsManagementRecord, + }, INDEX_PATTERN_FOO: { type: 'index-pattern', id: 'foo', @@ -76,7 +71,7 @@ const OBJECTS = { hiddenType: false, }, references: [], - } as SavedObjectsManagementRecord, + }, INDEX_PATTERN_BAR: { type: 'index-pattern', id: 'bar', @@ -87,7 +82,7 @@ const OBJECTS = { hiddenType: false, }, references: [], - } as SavedObjectsManagementRecord, + }, }; interface ObjectProperties { diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts index 9274053ce81ec..206952774ff9a 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/lib/summarize_copy_result.ts @@ -9,11 +9,8 @@ import type { SavedObjectsImportAmbiguousConflictError, SavedObjectsImportConflictError, } from 'src/core/public'; -import type { - FailedImport, - ProcessedImportResponse, -} from 'src/plugins/saved_objects_management/public'; +import type { FailedImport, ProcessedImportResponse } from '../lib'; import type { CopyToSpaceSavedObjectTarget } from '../types'; export interface SummarizedSavedObjectResult { diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index b55058d59b3fd..bf2c6e7fc8694 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -15,7 +15,6 @@ { "path": "../../../src/plugins/home/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/management/tsconfig.json" }, - { "path": "../../../src/plugins/saved_objects_management/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" } ] } From 1758d1268099a866a4084351ce1fa4894894420b Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Wed, 18 Aug 2021 16:05:12 -0400 Subject: [PATCH 07/28] Fix ts refs...? --- src/plugins/dashboard/tsconfig.json | 2 +- src/plugins/saved_objects_management/tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 5b77b215a9f69..7558ade4705be 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -14,7 +14,6 @@ "server/**/*", ], "references": [ - { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, { "path": "../../core/tsconfig.json" }, { "path": "../inspector/tsconfig.json" }, { "path": "../kibana_legacy/tsconfig.json" }, @@ -34,5 +33,6 @@ { "path": "../charts/tsconfig.json" }, { "path": "../discover/tsconfig.json" }, { "path": "../visualizations/tsconfig.json" }, + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, ] } diff --git a/src/plugins/saved_objects_management/tsconfig.json b/src/plugins/saved_objects_management/tsconfig.json index f8c11db8361c2..545d4697ca2cd 100644 --- a/src/plugins/saved_objects_management/tsconfig.json +++ b/src/plugins/saved_objects_management/tsconfig.json @@ -13,7 +13,6 @@ ], "references": [ { "path": "../../core/tsconfig.json" }, - { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, { "path": "../dashboard/tsconfig.json" }, { "path": "../data/tsconfig.json" }, { "path": "../discover/tsconfig.json" }, @@ -21,5 +20,6 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../management/tsconfig.json" }, { "path": "../visualizations/tsconfig.json" }, + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, ] } From 8c8dca6ad2b3c6a4017b98880cb80f10443889d3 Mon Sep 17 00:00:00 2001 From: mgiota Date: Wed, 18 Aug 2021 22:29:24 +0200 Subject: [PATCH 08/28] [RAC] [Observability] Use simpler alert severity level mapping (#109068) * [RAC][Observability] remove severity fields from mapping keep only ALERT_SEVERITY * temporarily remove severity value occurences * remove ALERT_SEVERITY_VALUE occurences, this value is not being read and shown in the Observability alerts table * remove duplicate ALERT_SEVERITY identifier Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/technical_field_names.ts | 6 ------ .../components/alerting/register_apm_alerts.ts | 8 ++++---- .../charts/helper/get_alert_annotations.test.tsx | 6 +++--- .../charts/helper/get_alert_annotations.tsx | 8 ++++---- .../latency_chart/latency_chart.stories.tsx | 6 +++--- ...ter_transaction_duration_anomaly_alert_type.ts | 12 ++++-------- .../public/pages/alerts/example_data.ts | 15 +++------------ .../public/pages/alerts/render_cell_value.tsx | 8 ++++---- .../assets/field_maps/technical_rule_field_map.ts | 3 +-- .../server/lib/alerts/duration_anomaly.test.ts | 6 ++---- .../uptime/server/lib/alerts/duration_anomaly.ts | 6 ++---- .../uptime/server/lib/alerts/status_check.test.ts | 6 +++--- .../uptime/server/lib/alerts/status_check.ts | 4 ++-- .../plugins/uptime/server/lib/alerts/tls.test.ts | 4 ++-- x-pack/plugins/uptime/server/lib/alerts/tls.ts | 4 ++-- 15 files changed, 39 insertions(+), 63 deletions(-) diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 2aa23195df899..fa3d61d00529c 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -32,8 +32,6 @@ const ALERT_ID = `${ALERT_NAMESPACE}.id` as const; const ALERT_REASON = `${ALERT_NAMESPACE}.reason` as const; const ALERT_RISK_SCORE = `${ALERT_NAMESPACE}.risk_score` as const; const ALERT_SEVERITY = `${ALERT_NAMESPACE}.severity` as const; -const ALERT_SEVERITY_LEVEL = `${ALERT_NAMESPACE}.severity.level` as const; -const ALERT_SEVERITY_VALUE = `${ALERT_NAMESPACE}.severity.value` as const; const ALERT_START = `${ALERT_NAMESPACE}.start` as const; const ALERT_STATUS = `${ALERT_NAMESPACE}.status` as const; const ALERT_SYSTEM_STATUS = `${ALERT_NAMESPACE}.system_status` as const; @@ -127,8 +125,6 @@ const fields = { ALERT_RULE_VERSION, ALERT_START, ALERT_SEVERITY, - ALERT_SEVERITY_LEVEL, - ALERT_SEVERITY_VALUE, ALERT_STATUS, ALERT_SYSTEM_STATUS, ALERT_UUID, @@ -183,8 +179,6 @@ export { ALERT_RULE_VERSION, ALERT_RULE_SEVERITY, ALERT_SEVERITY, - ALERT_SEVERITY_LEVEL, - ALERT_SEVERITY_VALUE, ALERT_START, ALERT_SYSTEM_STATUS, ALERT_UUID, diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index 5905f700b0bbe..dc52d572e2f35 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -11,12 +11,12 @@ import { stringify } from 'querystring'; import type { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, // @ts-expect-error } from '@kbn/rule-data-utils/target_node/technical_field_names'; import type { ObservabilityRuleTypeRegistry } from '../../../../observability/public'; @@ -36,7 +36,7 @@ const TRANSACTION_TYPE = 'transaction.type'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; const format = ({ pathname, @@ -211,7 +211,7 @@ export function registerApmAlerts( format: ({ fields }) => ({ reason: formatTransactionDurationAnomalyReason({ serviceName: String(fields[SERVICE_NAME][0]), - severityLevel: String(fields[ALERT_SEVERITY_LEVEL]), + severityLevel: String(fields[ALERT_SEVERITY]), measured: Number(fields[ALERT_EVALUATION_VALUE]), }), link: format({ diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx index 0f09b042a587b..25a37570182bf 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.test.tsx @@ -13,7 +13,7 @@ import { ALERT_ID, ALERT_RULE_PRODUCER, ALERT_RULE_CONSUMER, - ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY, ALERT_START, ALERT_STATUS, ALERT_UUID, @@ -163,7 +163,7 @@ describe('getAlertAnnotations', () => { describe('with an alert with a warning severity', () => { const warningAlert: Alert = { ...alert, - [ALERT_SEVERITY_LEVEL]: ['warning'], + [ALERT_SEVERITY]: ['warning'], }; it('uses the warning color', () => { @@ -196,7 +196,7 @@ describe('getAlertAnnotations', () => { describe('with an alert with a critical severity', () => { const criticalAlert: Alert = { ...alert, - [ALERT_SEVERITY_LEVEL]: ['critical'], + [ALERT_SEVERITY]: ['critical'], }; it('uses the critical color', () => { diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx index f51494b8fa1d8..4aef5f6e56b96 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/get_alert_annotations.tsx @@ -14,7 +14,7 @@ import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { ALERT_DURATION as ALERT_DURATION_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_TYPED, ALERT_START as ALERT_START_TYPED, ALERT_UUID as ALERT_UUID_TYPED, ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_TYPED, @@ -22,7 +22,7 @@ import type { } from '@kbn/rule-data-utils'; import { ALERT_DURATION as ALERT_DURATION_NON_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, ALERT_START as ALERT_START_NON_TYPED, ALERT_UUID as ALERT_UUID_NON_TYPED, ALERT_RULE_TYPE_ID as ALERT_RULE_TYPE_ID_NON_TYPED, @@ -38,7 +38,7 @@ import { asDuration, asPercent } from '../../../../../common/utils/formatters'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; -const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; const ALERT_START: typeof ALERT_START_TYPED = ALERT_START_NON_TYPED; const ALERT_UUID: typeof ALERT_UUID_TYPED = ALERT_UUID_NON_TYPED; const ALERT_RULE_TYPE_ID: typeof ALERT_RULE_TYPE_ID_TYPED = ALERT_RULE_TYPE_ID_NON_TYPED; @@ -119,7 +119,7 @@ export function getAlertAnnotations({ new Date(parsed[ALERT_START]!).getTime() ); const end = start + parsed[ALERT_DURATION]! / 1000; - const severityLevel = parsed[ALERT_SEVERITY_LEVEL]; + const severityLevel = parsed[ALERT_SEVERITY]; const color = getAlertColor({ severityLevel, theme }); const header = getAlertHeader({ severityLevel }); const formatter = getFormatter(parsed[ALERT_RULE_TYPE_ID]!); diff --git a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx index f9b22c422e3e3..17fdef952658d 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/latency_chart/latency_chart.stories.tsx @@ -11,7 +11,7 @@ import { ALERT_RULE_TYPE_ID, ALERT_EVALUATION_VALUE, ALERT_ID, - ALERT_SEVERITY_LEVEL, + ALERT_SEVERITY, ALERT_START, ALERT_STATUS, ALERT_UUID, @@ -158,7 +158,7 @@ Example.args = { tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], [ALERT_RULE_PRODUCER]: ['apm'], - [ALERT_SEVERITY_LEVEL]: ['warning'], + [ALERT_SEVERITY]: ['warning'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478181'], [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], @@ -180,7 +180,7 @@ Example.args = { tags: ['apm', 'service.name:frontend-rum'], 'transaction.type': ['page-load'], [ALERT_RULE_PRODUCER]: ['apm'], - [ALERT_SEVERITY_LEVEL]: ['critical'], + [ALERT_SEVERITY]: ['critical'], [ALERT_UUID]: ['af2ae371-df79-4fca-b0eb-a2dbd9478182'], [ALERT_RULE_UUID]: ['82e0ee40-c2f4-11eb-9a42-a9da66a1722f'], 'event.action': ['active'], diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index e38262773b6db..7d49833c01abf 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -12,15 +12,13 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import type { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, - ALERT_SEVERITY_VALUE as ALERT_SEVERITY_VALUE_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_TYPED, ALERT_REASON as ALERT_REASON_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_NON_TYPED, ALERT_EVALUATION_VALUE as ALERT_EVALUATION_VALUE_NON_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, - ALERT_SEVERITY_VALUE as ALERT_SEVERITY_VALUE_NON_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, ALERT_REASON as ALERT_REASON_NON_TYPED, // @ts-expect-error } from '@kbn/rule-data-utils/target_node/technical_field_names'; @@ -51,8 +49,7 @@ import { const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; const ALERT_EVALUATION_VALUE: typeof ALERT_EVALUATION_VALUE_TYPED = ALERT_EVALUATION_VALUE_NON_TYPED; -const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; -const ALERT_SEVERITY_VALUE: typeof ALERT_SEVERITY_VALUE_TYPED = ALERT_SEVERITY_VALUE_NON_TYPED; +const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; const paramsSchema = schema.object({ @@ -258,8 +255,7 @@ export function registerTransactionDurationAnomalyAlertType({ ...getEnvironmentEsField(environment), [TRANSACTION_TYPE]: transactionType, [PROCESSOR_EVENT]: ProcessorEvent.transaction, - [ALERT_SEVERITY_LEVEL]: severityLevel, - [ALERT_SEVERITY_VALUE]: score, + [ALERT_SEVERITY]: severityLevel, [ALERT_EVALUATION_VALUE]: score, [ALERT_EVALUATION_THRESHOLD]: threshold, [ALERT_REASON]: formatTransactionDurationAnomalyReason({ diff --git a/x-pack/plugins/observability/public/pages/alerts/example_data.ts b/x-pack/plugins/observability/public/pages/alerts/example_data.ts index 112932d49311c..535556a9b6ec1 100644 --- a/x-pack/plugins/observability/public/pages/alerts/example_data.ts +++ b/x-pack/plugins/observability/public/pages/alerts/example_data.ts @@ -9,8 +9,7 @@ import { ALERT_DURATION, ALERT_END, ALERT_ID, - ALERT_SEVERITY_LEVEL, - ALERT_SEVERITY_VALUE, + ALERT_SEVERITY, ALERT_RULE_TYPE_ID, ALERT_START, ALERT_STATUS, @@ -28,7 +27,7 @@ export const apmAlertResponseExample = [ [ALERT_RULE_NAME]: ['Error count threshold | opbeans-java (smith test)'], [ALERT_DURATION]: [180057000], [ALERT_STATUS]: ['open'], - [ALERT_SEVERITY_LEVEL]: ['warning'], + [ALERT_SEVERITY]: ['warning'], tags: ['apm', 'service.name:opbeans-java'], [ALERT_UUID]: ['0175ec0a-a3b1-4d41-b557-e21c2d024352'], [ALERT_RULE_UUID]: ['474920d0-93e9-11eb-ac86-0b455460de81'], @@ -123,21 +122,13 @@ export const dynamicIndexPattern = { readFromDocValues: true, }, { - name: ALERT_SEVERITY_LEVEL, + name: ALERT_SEVERITY, type: 'string', esTypes: ['keyword'], searchable: true, aggregatable: true, readFromDocValues: true, }, - { - name: ALERT_SEVERITY_VALUE, - type: 'number', - esTypes: ['long'], - searchable: true, - aggregatable: true, - readFromDocValues: true, - }, { name: ALERT_START, type: 'date', diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index 34b595ddc34f3..c85ea0b1086fa 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -14,13 +14,13 @@ import React, { useEffect } from 'react'; */ import type { ALERT_DURATION as ALERT_DURATION_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_TYPED, ALERT_STATUS as ALERT_STATUS_TYPED, ALERT_REASON as ALERT_REASON_TYPED, } from '@kbn/rule-data-utils'; import { ALERT_DURATION as ALERT_DURATION_NON_TYPED, - ALERT_SEVERITY_LEVEL as ALERT_SEVERITY_LEVEL_NON_TYPED, + ALERT_SEVERITY as ALERT_SEVERITY_NON_TYPED, ALERT_STATUS as ALERT_STATUS_NON_TYPED, ALERT_REASON as ALERT_REASON_NON_TYPED, TIMESTAMP, @@ -37,7 +37,7 @@ import { usePluginContext } from '../../hooks/use_plugin_context'; import { useTheme } from '../../hooks/use_theme'; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; -const ALERT_SEVERITY_LEVEL: typeof ALERT_SEVERITY_LEVEL_TYPED = ALERT_SEVERITY_LEVEL_NON_TYPED; +const ALERT_SEVERITY: typeof ALERT_SEVERITY_TYPED = ALERT_SEVERITY_NON_TYPED; const ALERT_STATUS: typeof ALERT_STATUS_TYPED = ALERT_STATUS_NON_TYPED; const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; @@ -118,7 +118,7 @@ export const getRenderCellValue = ({ return ; case ALERT_DURATION: return asDuration(Number(value)); - case ALERT_SEVERITY_LEVEL: + case ALERT_SEVERITY: return ; case ALERT_REASON: const dataFieldEs = data.reduce((acc, d) => ({ ...acc, [d.field]: d.value }), {}); diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index f6566ee75920f..b4ae89b7694f7 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -25,8 +25,7 @@ export const technicalRuleFieldMap = { [Fields.ALERT_START]: { type: 'date' }, [Fields.ALERT_END]: { type: 'date' }, [Fields.ALERT_DURATION]: { type: 'long' }, - [Fields.ALERT_SEVERITY_LEVEL]: { type: 'keyword' }, - [Fields.ALERT_SEVERITY_VALUE]: { type: 'long' }, + [Fields.ALERT_SEVERITY]: { type: 'keyword' }, [Fields.ALERT_STATUS]: { type: 'keyword' }, [Fields.ALERT_EVALUATION_THRESHOLD]: { type: 'scaled_float', scaling_factor: 100 }, [Fields.ALERT_EVALUATION_VALUE]: { type: 'scaled_float', scaling_factor: 100 }, diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts index d69453fd76b91..b87fbc2c45ac7 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts @@ -5,8 +5,7 @@ * 2.0. */ import { - ALERT_SEVERITY_LEVEL, - ALERT_SEVERITY_VALUE, + ALERT_SEVERITY, ALERT_EVALUATION_VALUE, ALERT_EVALUATION_THRESHOLD, ALERT_REASON, @@ -171,8 +170,7 @@ describe('duration anomaly alert', () => { 'observer.geo.name': anomaly.entityValue, [ALERT_EVALUATION_VALUE]: anomaly.actualSort, [ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort, - [ALERT_SEVERITY_LEVEL]: getSeverityType(anomaly.severity), - [ALERT_SEVERITY_VALUE]: anomaly.severity, + [ALERT_SEVERITY]: getSeverityType(anomaly.severity), [ALERT_REASON]: `Abnormal (${getSeverityType( anomaly.severity )} level) response time detected on uptime-monitor with url ${ diff --git a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts index 3da0fcf65cbe4..cf241386ec277 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.ts @@ -8,8 +8,7 @@ import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import moment from 'moment'; import { schema } from '@kbn/config-schema'; import { - ALERT_SEVERITY_LEVEL, - ALERT_SEVERITY_VALUE, + ALERT_SEVERITY, ALERT_EVALUATION_VALUE, ALERT_EVALUATION_THRESHOLD, ALERT_REASON, @@ -135,8 +134,7 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory 'anomaly.bucket_span.minutes': summary.bucketSpan, [ALERT_EVALUATION_VALUE]: anomaly.actualSort, [ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort, - [ALERT_SEVERITY_LEVEL]: summary.severity, - [ALERT_SEVERITY_VALUE]: summary.severityScore, + [ALERT_SEVERITY]: summary.severity, [ALERT_REASON]: generateAlertMessage( CommonDurationAnomalyTranslations.defaultActionMessage, summary diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts index 4cf7a566454c4..4afe7e829d058 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.test.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { ALERT_REASON, ALERT_SEVERITY_WARNING, ALERT_SEVERITY_LEVEL } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_SEVERITY_WARNING, ALERT_SEVERITY } from '@kbn/rule-data-utils'; import { generateFilterDSL, hasFilters, @@ -75,7 +75,7 @@ const mockStatusAlertDocument = ( [ALERT_REASON]: `Monitor first with url ${monitorInfo?.url?.full} is down from ${ monitorInfo.observer?.geo?.name }. The latest error message is ${monitorInfo.error?.message || ''}`, - [ALERT_SEVERITY_LEVEL]: ALERT_SEVERITY_WARNING, + [ALERT_SEVERITY]: ALERT_SEVERITY_WARNING, }, id: getInstanceId( monitorInfo, @@ -96,7 +96,7 @@ const mockAvailabilityAlertDocument = (monitor: GetMonitorAvailabilityResult) => )}% availability expected is 99.34% from ${ monitorInfo.observer?.geo?.name }. The latest error message is ${monitorInfo.error?.message || ''}`, - [ALERT_SEVERITY_LEVEL]: ALERT_SEVERITY_WARNING, + [ALERT_SEVERITY]: ALERT_SEVERITY_WARNING, }, id: getInstanceId(monitorInfo, `${monitorInfo?.monitor.id}-${monitorInfo.observer?.geo?.name}`), }; diff --git a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts index 4b00b7316b687..55de5069aada9 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/status_check.ts @@ -7,7 +7,7 @@ import { min } from 'lodash'; import datemath from '@elastic/datemath'; import { schema } from '@kbn/config-schema'; -import { ALERT_SEVERITY_WARNING, ALERT_SEVERITY_LEVEL } from '@kbn/rule-data-utils'; +import { ALERT_SEVERITY_WARNING, ALERT_SEVERITY } from '@kbn/rule-data-utils'; import { i18n } from '@kbn/i18n'; import { JsonObject } from '@kbn/utility-types'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; @@ -159,7 +159,7 @@ export const getMonitorAlertDocument = (monitorSummary: Record { 'tls.server.x509.not_after': cert.not_after, 'tls.server.x509.not_before': cert.not_before, 'tls.server.hash.sha256': cert.sha256, - [ALERT_SEVERITY_LEVEL]: ALERT_SEVERITY_WARNING, + [ALERT_SEVERITY]: ALERT_SEVERITY_WARNING, }), id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`, }); diff --git a/x-pack/plugins/uptime/server/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/lib/alerts/tls.ts index 88fa88b24d22e..63636ddfe5906 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/tls.ts @@ -6,7 +6,7 @@ */ import moment from 'moment'; import { schema } from '@kbn/config-schema'; -import { ALERT_REASON, ALERT_SEVERITY_WARNING, ALERT_SEVERITY_LEVEL } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_SEVERITY_WARNING, ALERT_SEVERITY } from '@kbn/rule-data-utils'; import { UptimeAlertTypeFactory } from './types'; import { updateState, generateAlertMessage } from './common'; import { TLS } from '../../../common/constants/alerts'; @@ -172,7 +172,7 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = (_server, 'tls.server.x509.not_after': cert.not_after, 'tls.server.x509.not_before': cert.not_before, 'tls.server.hash.sha256': cert.sha256, - [ALERT_SEVERITY_LEVEL]: ALERT_SEVERITY_WARNING, + [ALERT_SEVERITY]: ALERT_SEVERITY_WARNING, [ALERT_REASON]: generateAlertMessage(TlsTranslations.defaultActionMessage, summary), }, }); From 49ba7e746c9cb8123499d62bbcfa109a2d116bb8 Mon Sep 17 00:00:00 2001 From: Luke Elmers Date: Wed, 18 Aug 2021 14:46:17 -0600 Subject: [PATCH 09/28] Add deprecation notice to dashboard import/export docs. (#108826) --- docs/api/dashboard-api.asciidoc | 2 ++ docs/api/dashboard/export-dashboard.asciidoc | 4 +++- docs/api/dashboard/import-dashboard.asciidoc | 4 +++- docs/user/dashboard/dashboard.asciidoc | 2 +- src/plugins/legacy_export/README.md | 2 +- src/plugins/legacy_export/server/plugin.ts | 1 + 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/api/dashboard-api.asciidoc b/docs/api/dashboard-api.asciidoc index 50c2abc975763..e6f54dd9156ec 100644 --- a/docs/api/dashboard-api.asciidoc +++ b/docs/api/dashboard-api.asciidoc @@ -1,6 +1,8 @@ [[dashboard-api]] == Import and export dashboard APIs +deprecated::[7.15.0,Both of these APIs have been deprecated in favor of <> and <>.] + Import and export dashboards with the corresponding saved objects, such as visualizations, saved searches, and index patterns. diff --git a/docs/api/dashboard/export-dashboard.asciidoc b/docs/api/dashboard/export-dashboard.asciidoc index 6d239d755eb0d..3a20eff0a54d2 100644 --- a/docs/api/dashboard/export-dashboard.asciidoc +++ b/docs/api/dashboard/export-dashboard.asciidoc @@ -4,7 +4,9 @@ Export dashboard ++++ -experimental[] Export dashboards and corresponding saved objects. +deprecated::[7.15.0,Use <> instead.] + +Export dashboards and corresponding saved objects. [[dashboard-api-export-request]] ==== Request diff --git a/docs/api/dashboard/import-dashboard.asciidoc b/docs/api/dashboard/import-dashboard.asciidoc index 5d1fab41a2a14..e4817d6cb7ee9 100644 --- a/docs/api/dashboard/import-dashboard.asciidoc +++ b/docs/api/dashboard/import-dashboard.asciidoc @@ -4,7 +4,9 @@ Import dashboard ++++ -experimental[] Import dashboards and corresponding saved objects. +deprecated::[7.15.0,Use <> instead.] + +Import dashboards and corresponding saved objects. [[dashboard-api-import-request]] ==== Request diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 374dc4f735e9b..6430c5d246dc6 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -345,7 +345,7 @@ To share the dashboard with a larger audience, click *Share* in the toolbar. For [[import-dashboards]] == Export dashboards -To automate {kib}, you can export dashboards as JSON using the <>. It is important to export dashboards with all necessary references. +To automate {kib}, you can export dashboards as NDJSON using the <>. It is important to export dashboards with all necessary references. -- include::tutorial-create-a-dashboard-of-lens-panels.asciidoc[] diff --git a/src/plugins/legacy_export/README.md b/src/plugins/legacy_export/README.md index 050e39b8f19e4..551487a1122fc 100644 --- a/src/plugins/legacy_export/README.md +++ b/src/plugins/legacy_export/README.md @@ -1,3 +1,3 @@ -# `legacyExport` plugin +# `legacyExport` plugin [deprecated] The `legacyExport` plugin adds support for the legacy saved objects export format. diff --git a/src/plugins/legacy_export/server/plugin.ts b/src/plugins/legacy_export/server/plugin.ts index ac38f300bd02b..a6bdcdc19b0a1 100644 --- a/src/plugins/legacy_export/server/plugin.ts +++ b/src/plugins/legacy_export/server/plugin.ts @@ -9,6 +9,7 @@ import { Plugin, CoreSetup, PluginInitializerContext } from 'kibana/server'; import { registerRoutes } from './routes'; +/** @deprecated */ export class LegacyExportPlugin implements Plugin<{}, {}> { constructor(private readonly initContext: PluginInitializerContext) {} From 83a772019647c255817a786d94a0351efff1aba9 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Wed, 18 Aug 2021 16:56:36 -0500 Subject: [PATCH 10/28] [Workplace Search] Fix rendering bug on Private Source status (#109122) * Fix rendering bug on Private Source status * Fix rendering bug on Private Source status * Refactor per code review --- .../components/source_info_card.test.tsx | 2 +- .../content_sources/components/source_info_card.tsx | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx index 7c4c02cdc9819..c779d76af5e75 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx @@ -29,7 +29,7 @@ describe('SourceInfoCard', () => { expect(wrapper.find(SourceIcon)).toHaveLength(1); expect(wrapper.find(EuiBadge)).toHaveLength(1); expect(wrapper.find(EuiHealth)).toHaveLength(1); - expect(wrapper.find(EuiText)).toHaveLength(3); + expect(wrapper.find(EuiText)).toHaveLength(1); expect(wrapper.find(EuiTitle)).toHaveLength(1); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx index d98b4f6b1e67d..e2c9cc05b04c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -64,16 +64,12 @@ export const SourceInfoCard: React.FC = ({ {isFederatedSource && ( - + - - {STATUS_LABEL} - + {STATUS_LABEL} - - {READY_TEXT} - + {READY_TEXT} )} From 447ae98bc8236720be14ddbe30244d42095659e0 Mon Sep 17 00:00:00 2001 From: Candace Park <56409205+parkiino@users.noreply.github.com> Date: Wed, 18 Aug 2021 18:20:31 -0400 Subject: [PATCH 11/28] [Security Solution][Endpoint][Event Filters] Event filters updated UI (#109039) --- .../exception_item/exception_details.tsx | 47 ++++++++++++++----- .../exception_item/exception_entries.tsx | 21 +++++++-- .../viewer/exception_item/index.tsx | 2 +- .../view/components/empty/index.tsx | 2 +- .../view/event_filters_list_page.tsx | 10 ++-- .../pages/event_filters/view/translations.ts | 3 +- 6 files changed, 60 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx index 2bb6854739a32..3354637b9f745 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_details.tsx @@ -12,6 +12,7 @@ import { EuiButtonEmpty, EuiDescriptionListTitle, EuiDescriptionListDescription, + EuiToolTip, } from '@elastic/eui'; import React, { useMemo, Fragment } from 'react'; import styled, { css } from 'styled-components'; @@ -25,15 +26,28 @@ const MyExceptionDetails = styled(EuiFlexItem)` ${({ theme }) => css` background-color: ${theme.eui.euiColorLightestShade}; padding: ${theme.eui.euiSize}; + .eventFiltersDescriptionList { + margin: ${theme.eui.euiSize} ${theme.eui.euiSize} 0 ${theme.eui.euiSize}; + } + .eventFiltersDescriptionListTitle { + width: 40%; + margin-top: 0; + margin-bottom: ${theme.eui.euiSizeS}; + } + .eventFiltersDescriptionListDescription { + width: 60%; + margin-top: 0; + margin-bottom: ${theme.eui.euiSizeS}; + } `} `; -const MyDescriptionListTitle = styled(EuiDescriptionListTitle)` - width: 40%; -`; - -const MyDescriptionListDescription = styled(EuiDescriptionListDescription)` - width: 60%; +const StyledCommentsSection = styled(EuiFlexItem)` + ${({ theme }) => css` + &&& { + margin: ${theme.eui.euiSizeXS} ${theme.eui.euiSize} ${theme.eui.euiSizeL} ${theme.eui.euiSize}; + } + `} `; const ExceptionDetailsComponent = ({ @@ -77,19 +91,28 @@ const ExceptionDetailsComponent = ({ return ( - + {descriptionListItems.map((item) => ( - {item.title} - - {item.description} - + + + {item.title} + + + + + {item.description} + + ))} - {commentsSection} + {commentsSection} ); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx index 7429a934d557d..18b7298136302 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/exception_entries.tsx @@ -15,6 +15,7 @@ import { EuiHideFor, EuiBadge, EuiBadgeGroup, + EuiToolTip, } from '@elastic/eui'; import React, { useMemo } from 'react'; import styled, { css } from 'styled-components'; @@ -26,7 +27,12 @@ import * as i18n from '../../translations'; import { FormattedEntry } from '../../types'; const MyEntriesDetails = styled(EuiFlexItem)` - padding: ${({ theme }) => theme.eui.euiSize}; + ${({ theme }) => css` + padding: ${theme.eui.euiSize} ${theme.eui.euiSizeL} ${theme.eui.euiSizeL} ${theme.eui.euiSizeXS}; + &&& { + margin-left: 0; + } + `} `; const MyEditButton = styled(EuiButton)` @@ -46,8 +52,9 @@ const MyRemoveButton = styled(EuiButton)` `; const MyAndOrBadgeContainer = styled(EuiFlexItem)` - padding-top: ${({ theme }) => theme.eui.euiSizeXL}; - padding-bottom: ${({ theme }) => theme.eui.euiSizeS}; + ${({ theme }) => css` + padding: ${theme.eui.euiSizeXL} ${theme.eui.euiSize} ${theme.eui.euiSizeS} 0; + `} `; const MyActionButton = styled(EuiFlexItem)` @@ -132,7 +139,13 @@ const ExceptionEntriesComponent = ({ ); } else { - return values ?? getEmptyValue(); + return values ? ( + + {values} + + ) : ( + getEmptyValue() + ); } }, }, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx index b73442b04c9b4..6a53f47baf6b8 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.tsx @@ -84,7 +84,7 @@ const ExceptionItemComponent = ({ }, [loadingItemIds, exceptionItem.id]); return ( - + diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx index 9ad2549c85642..e27adc851dab7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/components/empty/index.tsx @@ -48,7 +48,7 @@ export const EventFiltersListEmptyState = memo<{ > } diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx index 8466e19100f73..e206f85df6548 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.tsx @@ -11,7 +11,7 @@ import { Dispatch } from 'redux'; import { useHistory, useLocation } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButton, EuiSpacer, EuiHorizontalRule, EuiText } from '@elastic/eui'; +import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; @@ -192,7 +192,7 @@ export const EventFiltersListPage = memo(() => { title={ } subtitle={ABOUT_EVENT_FILTERS} @@ -207,7 +207,7 @@ export const EventFiltersListPage = memo(() => { > ) @@ -236,11 +236,11 @@ export const EventFiltersListPage = memo(() => { - + )} diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts index 4c127ee47003f..ae8012711fbf1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/translations.ts @@ -54,6 +54,5 @@ export const getGetErrorMessage = (getError: ServerApiError) => { export const ABOUT_EVENT_FILTERS = i18n.translate('xpack.securitySolution.eventFilters.aboutInfo', { defaultMessage: - 'Add an event filter to exclude high volume or unwanted events from being written to Elasticsearch. Event ' + - 'filters are processed by the Endpoint Security integration, and are applied to hosts running this integration on their agents.', + 'Add an event filter to exclude high volume or unwanted events from being written to Elasticsearch.', }); From f719aa40319d316616069ba67c4ad921eeccc531 Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Wed, 18 Aug 2021 19:02:11 -0400 Subject: [PATCH 12/28] [Fleet] Missing migration backport in 7.14.0 (#109174) --- x-pack/plugins/fleet/server/saved_objects/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 54cb0846207a3..449a1984aa53b 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -322,6 +322,7 @@ const getSavedObjectTypes = ( }, migrations: { '7.14.0': migrateInstallationToV7140, + '7.14.1': migrateInstallationToV7140, }, }, [ASSETS_SAVED_OBJECT_TYPE]: { From 64454f32839c692849ebd72f37151ae73b5f3d6b Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Wed, 18 Aug 2021 20:05:16 -0500 Subject: [PATCH 13/28] Revert "[build] Remove optimize dir creation (#109014)" This reverts commit c2f310006df2aff38db5bd8608bc76b55a73c4b4. --- src/dev/build/tasks/create_empty_dirs_and_files_task.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dev/build/tasks/create_empty_dirs_and_files_task.ts b/src/dev/build/tasks/create_empty_dirs_and_files_task.ts index 6ef878cbab554..06b402c580151 100644 --- a/src/dev/build/tasks/create_empty_dirs_and_files_task.ts +++ b/src/dev/build/tasks/create_empty_dirs_and_files_task.ts @@ -12,6 +12,9 @@ export const CreateEmptyDirsAndFiles: Task = { description: 'Creating some empty directories and files to prevent file-permission issues', async run(config, log, build) { - await mkdirp(build.resolvePath('plugins')); + await Promise.all([ + mkdirp(build.resolvePath('plugins')), + mkdirp(build.resolvePath('data/optimize')), + ]); }, }; From 09f122b4782c9555815d16580e699fb556be00a4 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Wed, 18 Aug 2021 21:25:32 -0500 Subject: [PATCH 14/28] Increase width of service inventory sparkplot columns (#109137) From 160 to 176px. Prevents overlapping of the labels. Fixes #109103. --- .../components/app/service_inventory/service_list/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx index 8a623300da81a..8732084e6331e 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/index.tsx @@ -163,7 +163,7 @@ export function getServiceColumns({ /> ), align: 'left', - width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', + width: showWhenSmallOrGreaterThanLarge ? `${unit * 11}px` : 'auto', }, { field: 'throughput', @@ -184,7 +184,7 @@ export function getServiceColumns({ /> ), align: 'left', - width: showWhenSmallOrGreaterThanLarge ? `${unit * 10}px` : 'auto', + width: showWhenSmallOrGreaterThanLarge ? `${unit * 11}px` : 'auto', }, { field: 'transactionErrorRate', From 48ce73db15d813933b5295153231a6bc6ca41eb9 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Wed, 18 Aug 2021 22:32:19 -0400 Subject: [PATCH 15/28] [Alerting] Update rules detail page to resolve SO IDs if necessary (#108091) * Adding internal resolve API to resolve rules given an ID * Updating after merge * Updating after merge * Adding resolveRule api to client and adding spacesOss plugin dependency * Handling 404 errors by calling resolve. Updating unit tests * Handling aliasMatch and conflict results * Fixing types * Unit tests for spaces oss components * Adding audit event for resolve Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/alerting/common/alert.ts | 8 +- .../plugins/alerting/server/routes/index.ts | 2 + .../server/routes/resolve_rule.test.ts | 182 +++++++ .../alerting/server/routes/resolve_rule.ts | 84 ++++ .../alerting/server/rules_client.mock.ts | 1 + .../server/rules_client/audit_events.ts | 3 + .../server/rules_client/rules_client.ts | 47 ++ .../server/rules_client/tests/resolve.test.ts | 451 ++++++++++++++++++ .../plugins/triggers_actions_ui/kibana.json | 2 +- .../public/application/app.tsx | 2 + .../lib/alert_api/common_transformations.ts | 15 +- .../public/application/lib/alert_api/index.ts | 1 + .../lib/alert_api/resolve_rule.test.ts | 104 ++++ .../application/lib/alert_api/resolve_rule.ts | 23 + .../components/alert_details_route.test.tsx | 336 +++++++++---- .../components/alert_details_route.tsx | 105 +++- .../with_bulk_alert_api_operations.test.tsx | 19 + .../with_bulk_alert_api_operations.tsx | 4 + .../common/lib/kibana/kibana_react.mock.ts | 3 +- .../triggers_actions_ui/public/plugin.ts | 3 + .../triggers_actions_ui/public/types.ts | 3 + .../plugins/triggers_actions_ui/tsconfig.json | 1 + 22 files changed, 1287 insertions(+), 112 deletions(-) create mode 100644 x-pack/plugins/alerting/server/routes/resolve_rule.test.ts create mode 100644 x-pack/plugins/alerting/server/routes/resolve_rule.ts create mode 100644 x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.ts diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index 64c759752faec..1274e7b95b114 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { SavedObjectAttribute, SavedObjectAttributes } from 'kibana/server'; +import { + SavedObjectAttribute, + SavedObjectAttributes, + SavedObjectsResolveResponse, +} from 'kibana/server'; import { AlertNotifyWhenType } from './alert_notify_when_type'; export type AlertTypeState = Record; @@ -76,6 +80,8 @@ export interface Alert { } export type SanitizedAlert = Omit, 'apiKey'>; +export type ResolvedSanitizedRule = SanitizedAlert & + Omit; export type SanitizedRuleConfig = Pick< SanitizedAlert, diff --git a/x-pack/plugins/alerting/server/routes/index.ts b/x-pack/plugins/alerting/server/routes/index.ts index ad1c97efe2334..c1c7eae45109e 100644 --- a/x-pack/plugins/alerting/server/routes/index.ts +++ b/x-pack/plugins/alerting/server/routes/index.ts @@ -22,6 +22,7 @@ import { findRulesRoute } from './find_rules'; import { getRuleAlertSummaryRoute } from './get_rule_alert_summary'; import { getRuleStateRoute } from './get_rule_state'; import { healthRoute } from './health'; +import { resolveRuleRoute } from './resolve_rule'; import { ruleTypesRoute } from './rule_types'; import { muteAllRuleRoute } from './mute_all_rule'; import { muteAlertRoute } from './mute_alert'; @@ -42,6 +43,7 @@ export function defineRoutes(opts: RouteOptions) { defineLegacyRoutes(opts); createRuleRoute(opts); getRuleRoute(router, licenseState); + resolveRuleRoute(router, licenseState); updateRuleRoute(router, licenseState); deleteRuleRoute(router, licenseState); aggregateRulesRoute(router, licenseState); diff --git a/x-pack/plugins/alerting/server/routes/resolve_rule.test.ts b/x-pack/plugins/alerting/server/routes/resolve_rule.test.ts new file mode 100644 index 0000000000000..b03369a74b865 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/resolve_rule.test.ts @@ -0,0 +1,182 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { pick } from 'lodash'; +import { resolveRuleRoute } from './resolve_rule'; +import { httpServiceMock } from 'src/core/server/mocks'; +import { licenseStateMock } from '../lib/license_state.mock'; +import { verifyApiAccess } from '../lib/license_api_access'; +import { mockHandlerArguments } from './_mock_handler_arguments'; +import { rulesClientMock } from '../rules_client.mock'; +import { ResolvedSanitizedRule } from '../types'; +import { AsApiContract } from './lib'; + +const rulesClient = rulesClientMock.create(); +jest.mock('../lib/license_api_access.ts', () => ({ + verifyApiAccess: jest.fn(), +})); + +beforeEach(() => { + jest.resetAllMocks(); +}); + +describe('resolveRuleRoute', () => { + const mockedRule: ResolvedSanitizedRule<{ + bar: boolean; + }> = { + id: '1', + alertTypeId: '1', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date(), + updatedAt: new Date(), + actions: [ + { + group: 'default', + id: '2', + actionTypeId: 'test', + params: { + foo: true, + }, + }, + ], + consumer: 'bar', + name: 'abc', + tags: ['foo'], + enabled: true, + muteAll: false, + notifyWhen: 'onActionGroupChange', + createdBy: '', + updatedBy: '', + apiKeyOwner: '', + throttle: '30s', + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }; + + const resolveResult: AsApiContract> = { + ...pick( + mockedRule, + 'consumer', + 'name', + 'schedule', + 'tags', + 'params', + 'throttle', + 'enabled', + 'alias_target_id' + ), + rule_type_id: mockedRule.alertTypeId, + notify_when: mockedRule.notifyWhen, + mute_all: mockedRule.muteAll, + created_by: mockedRule.createdBy, + updated_by: mockedRule.updatedBy, + api_key_owner: mockedRule.apiKeyOwner, + muted_alert_ids: mockedRule.mutedInstanceIds, + created_at: mockedRule.createdAt, + updated_at: mockedRule.updatedAt, + id: mockedRule.id, + execution_status: { + status: mockedRule.executionStatus.status, + last_execution_date: mockedRule.executionStatus.lastExecutionDate, + }, + actions: [ + { + group: mockedRule.actions[0].group, + id: mockedRule.actions[0].id, + params: mockedRule.actions[0].params, + connector_type_id: mockedRule.actions[0].actionTypeId, + }, + ], + outcome: 'aliasMatch', + }; + + it('resolves a rule with proper parameters', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + resolveRuleRoute(router, licenseState); + const [config, handler] = router.get.mock.calls[0]; + + expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rule/{id}/_resolve"`); + + rulesClient.resolve.mockResolvedValueOnce(mockedRule); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + await handler(context, req, res); + + expect(rulesClient.resolve).toHaveBeenCalledTimes(1); + expect(rulesClient.resolve.mock.calls[0][0].id).toEqual('1'); + + expect(res.ok).toHaveBeenCalledWith({ + body: resolveResult, + }); + }); + + it('ensures the license allows resolving rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + resolveRuleRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + rulesClient.resolve.mockResolvedValueOnce(mockedRule); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + await handler(context, req, res); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); + + it('ensures the license check prevents getting rules', async () => { + const licenseState = licenseStateMock.create(); + const router = httpServiceMock.createRouter(); + + (verifyApiAccess as jest.Mock).mockImplementation(() => { + throw new Error('OMG'); + }); + + resolveRuleRoute(router, licenseState); + + const [, handler] = router.get.mock.calls[0]; + + rulesClient.resolve.mockResolvedValueOnce(mockedRule); + + const [context, req, res] = mockHandlerArguments( + { rulesClient }, + { + params: { id: '1' }, + }, + ['ok'] + ); + + expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`); + + expect(verifyApiAccess).toHaveBeenCalledWith(licenseState); + }); +}); diff --git a/x-pack/plugins/alerting/server/routes/resolve_rule.ts b/x-pack/plugins/alerting/server/routes/resolve_rule.ts new file mode 100644 index 0000000000000..011d28780e718 --- /dev/null +++ b/x-pack/plugins/alerting/server/routes/resolve_rule.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { omit } from 'lodash'; +import { schema } from '@kbn/config-schema'; +import { IRouter } from 'kibana/server'; +import { ILicenseState } from '../lib'; +import { verifyAccessAndContext, RewriteResponseCase } from './lib'; +import { + AlertTypeParams, + AlertingRequestHandlerContext, + INTERNAL_BASE_ALERTING_API_PATH, + ResolvedSanitizedRule, +} from '../types'; + +const paramSchema = schema.object({ + id: schema.string(), +}); + +const rewriteBodyRes: RewriteResponseCase> = ({ + alertTypeId, + createdBy, + updatedBy, + createdAt, + updatedAt, + apiKeyOwner, + notifyWhen, + muteAll, + mutedInstanceIds, + executionStatus, + actions, + scheduledTaskId, + ...rest +}) => ({ + ...rest, + rule_type_id: alertTypeId, + created_by: createdBy, + updated_by: updatedBy, + created_at: createdAt, + updated_at: updatedAt, + api_key_owner: apiKeyOwner, + notify_when: notifyWhen, + mute_all: muteAll, + muted_alert_ids: mutedInstanceIds, + scheduled_task_id: scheduledTaskId, + execution_status: executionStatus && { + ...omit(executionStatus, 'lastExecutionDate'), + last_execution_date: executionStatus.lastExecutionDate, + }, + actions: actions.map(({ group, id, actionTypeId, params }) => ({ + group, + id, + params, + connector_type_id: actionTypeId, + })), +}); + +export const resolveRuleRoute = ( + router: IRouter, + licenseState: ILicenseState +) => { + router.get( + { + path: `${INTERNAL_BASE_ALERTING_API_PATH}/rule/{id}/_resolve`, + validate: { + params: paramSchema, + }, + }, + router.handleLegacyErrors( + verifyAccessAndContext(licenseState, async function (context, req, res) { + const rulesClient = context.alerting.getRulesClient(); + const { id } = req.params; + const rule = await rulesClient.resolve({ id }); + return res.ok({ + body: rewriteBodyRes(rule), + }); + }) + ) + ); +}; diff --git a/x-pack/plugins/alerting/server/rules_client.mock.ts b/x-pack/plugins/alerting/server/rules_client.mock.ts index 4bd197e51a5da..438331a1cd580 100644 --- a/x-pack/plugins/alerting/server/rules_client.mock.ts +++ b/x-pack/plugins/alerting/server/rules_client.mock.ts @@ -16,6 +16,7 @@ const createRulesClientMock = () => { aggregate: jest.fn(), create: jest.fn(), get: jest.fn(), + resolve: jest.fn(), getAlertState: jest.fn(), find: jest.fn(), delete: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/audit_events.ts b/x-pack/plugins/alerting/server/rules_client/audit_events.ts index f04b7c3701974..5f6122458ddaf 100644 --- a/x-pack/plugins/alerting/server/rules_client/audit_events.ts +++ b/x-pack/plugins/alerting/server/rules_client/audit_events.ts @@ -11,6 +11,7 @@ import { AuditEvent } from '../../../security/server'; export enum RuleAuditAction { CREATE = 'rule_create', GET = 'rule_get', + RESOLVE = 'rule_resolve', UPDATE = 'rule_update', UPDATE_API_KEY = 'rule_update_api_key', ENABLE = 'rule_enable', @@ -28,6 +29,7 @@ type VerbsTuple = [string, string, string]; const eventVerbs: Record = { rule_create: ['create', 'creating', 'created'], rule_get: ['access', 'accessing', 'accessed'], + rule_resolve: ['access', 'accessing', 'accessed'], rule_update: ['update', 'updating', 'updated'], rule_update_api_key: ['update API key of', 'updating API key of', 'updated API key of'], rule_enable: ['enable', 'enabling', 'enabled'], @@ -43,6 +45,7 @@ const eventVerbs: Record = { const eventTypes: Record = { rule_create: 'creation', rule_get: 'access', + rule_resolve: 'access', rule_update: 'change', rule_update_api_key: 'change', rule_enable: 'change', diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index a079a52448e2d..4d191584592a2 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -33,6 +33,7 @@ import { AlertExecutionStatusValues, AlertNotifyWhenType, AlertTypeParams, + ResolvedSanitizedRule, } from '../types'; import { validateAlertTypeParams, @@ -411,6 +412,52 @@ export class RulesClient { ); } + public async resolve({ + id, + }: { + id: string; + }): Promise> { + const { + saved_object: result, + ...resolveResponse + } = await this.unsecuredSavedObjectsClient.resolve('alert', id); + try { + await this.authorization.ensureAuthorized({ + ruleTypeId: result.attributes.alertTypeId, + consumer: result.attributes.consumer, + operation: ReadOperations.Get, + entity: AlertingAuthorizationEntity.Rule, + }); + } catch (error) { + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.RESOLVE, + savedObject: { type: 'alert', id }, + error, + }) + ); + throw error; + } + this.auditLogger?.log( + ruleAuditEvent({ + action: RuleAuditAction.RESOLVE, + savedObject: { type: 'alert', id }, + }) + ); + + const rule = this.getAlertFromRaw( + result.id, + result.attributes.alertTypeId, + result.attributes, + result.references + ); + + return { + ...rule, + ...resolveResponse, + }; + } + public async getAlertState({ id }: { id: string }): Promise { const alert = await this.get({ id }); await this.authorization.ensureAuthorized({ diff --git a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts new file mode 100644 index 0000000000000..63feb4ff3147a --- /dev/null +++ b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts @@ -0,0 +1,451 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RulesClient, ConstructorOptions } from '../rules_client'; +import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { taskManagerMock } from '../../../../task_manager/server/mocks'; +import { ruleTypeRegistryMock } from '../../rule_type_registry.mock'; +import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock'; +import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks'; +import { actionsAuthorizationMock } from '../../../../actions/server/mocks'; +import { AlertingAuthorization } from '../../authorization/alerting_authorization'; +import { ActionsAuthorization } from '../../../../actions/server'; +import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { auditServiceMock } from '../../../../security/server/audit/index.mock'; +import { getBeforeSetup, setGlobalDate } from './lib'; +import { RecoveredActionGroup } from '../../../common'; + +const taskManager = taskManagerMock.createStart(); +const ruleTypeRegistry = ruleTypeRegistryMock.create(); +const unsecuredSavedObjectsClient = savedObjectsClientMock.create(); +const encryptedSavedObjects = encryptedSavedObjectsMock.createClient(); +const authorization = alertingAuthorizationMock.create(); +const actionsAuthorization = actionsAuthorizationMock.create(); +const auditLogger = auditServiceMock.create().asScoped(httpServerMock.createKibanaRequest()); + +const kibanaVersion = 'v7.10.0'; +const rulesClientParams: jest.Mocked = { + taskManager, + ruleTypeRegistry, + unsecuredSavedObjectsClient, + authorization: (authorization as unknown) as AlertingAuthorization, + actionsAuthorization: (actionsAuthorization as unknown) as ActionsAuthorization, + spaceId: 'default', + namespace: 'default', + getUserName: jest.fn(), + createAPIKey: jest.fn(), + logger: loggingSystemMock.create().get(), + encryptedSavedObjectsClient: encryptedSavedObjects, + getActionsClient: jest.fn(), + getEventLogClient: jest.fn(), + kibanaVersion, +}; + +beforeEach(() => { + getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry); + (auditLogger.log as jest.Mock).mockClear(); +}); + +setGlobalDate(); + +describe('resolve()', () => { + test('calls saved objects client with given params', async () => { + const rulesClient = new RulesClient(rulesClientParams); + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + notifyWhen: 'onActiveAlert', + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + const result = await rulesClient.resolve({ id: '1' }); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alias_target_id": "2", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "notifyWhen": "onActiveAlert", + "outcome": "aliasMatch", + "params": Object { + "bar": true, + }, + "schedule": Object { + "interval": "10s", + }, + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + expect(unsecuredSavedObjectsClient.resolve).toHaveBeenCalledTimes(1); + expect(unsecuredSavedObjectsClient.resolve.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "alert", + "1", + ] + `); + }); + + test('should call useSavedObjectReferences.injectReferences if defined for rule type', async () => { + const injectReferencesFn = jest.fn().mockReturnValue({ + bar: true, + parameterThatIsSavedObjectId: '9', + }); + ruleTypeRegistry.get.mockImplementation(() => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: RecoveredActionGroup, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + async executor() {}, + producer: 'alerts', + useSavedObjectReferences: { + extractReferences: jest.fn(), + injectReferences: injectReferencesFn, + }, + })); + const rulesClient = new RulesClient(rulesClientParams); + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + parameterThatIsSavedObjectRef: 'soRef_0', + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + notifyWhen: 'onActiveAlert', + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'param:soRef_0', + type: 'someSavedObjectType', + id: '9', + }, + ], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + const result = await rulesClient.resolve({ id: '1' }); + + expect(injectReferencesFn).toHaveBeenCalledWith( + { + bar: true, + parameterThatIsSavedObjectRef: 'soRef_0', + }, + [{ id: '9', name: 'soRef_0', type: 'someSavedObjectType' }] + ); + expect(result).toMatchInlineSnapshot(` + Object { + "actions": Array [ + Object { + "group": "default", + "id": "1", + "params": Object { + "foo": true, + }, + }, + ], + "alertTypeId": "123", + "alias_target_id": "2", + "createdAt": 2019-02-12T21:01:22.479Z, + "id": "1", + "notifyWhen": "onActiveAlert", + "outcome": "aliasMatch", + "params": Object { + "bar": true, + "parameterThatIsSavedObjectId": "9", + }, + "schedule": Object { + "interval": "10s", + }, + "updatedAt": 2019-02-12T21:01:22.479Z, + } + `); + }); + + test(`throws an error when references aren't found`, async () => { + const rulesClient = new RulesClient(rulesClientParams); + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + await expect(rulesClient.resolve({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Action reference \\"action_0\\" not found in alert id: 1"` + ); + }); + + test('throws an error if useSavedObjectReferences.injectReferences throws an error', async () => { + const injectReferencesFn = jest.fn().mockImplementation(() => { + throw new Error('something went wrong!'); + }); + ruleTypeRegistry.get.mockImplementation(() => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: RecoveredActionGroup, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + async executor() {}, + producer: 'alerts', + useSavedObjectReferences: { + extractReferences: jest.fn(), + injectReferences: injectReferencesFn, + }, + })); + const rulesClient = new RulesClient(rulesClientParams); + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + parameterThatIsSavedObjectRef: 'soRef_0', + }, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + notifyWhen: 'onActiveAlert', + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + { + name: 'soRef_0', + type: 'someSavedObjectType', + id: '9', + }, + ], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + await expect(rulesClient.resolve({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error injecting reference into rule params for rule id 1 - something went wrong!"` + ); + }); + + describe('authorization', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: 'myType', + consumer: 'myApp', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [ + { + group: 'default', + actionRef: 'action_0', + params: { + foo: true, + }, + }, + ], + }, + references: [ + { + name: 'action_0', + type: 'action', + id: '1', + }, + ], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + }); + + test('ensures user is authorised to resolve this type of rule under the consumer', async () => { + const rulesClient = new RulesClient(rulesClientParams); + await rulesClient.resolve({ id: '1' }); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'rule', + consumer: 'myApp', + operation: 'get', + ruleTypeId: 'myType', + }); + }); + + test('throws when user is not authorised to get this type of alert', async () => { + const rulesClient = new RulesClient(rulesClientParams); + authorization.ensureAuthorized.mockRejectedValue( + new Error(`Unauthorized to get a "myType" alert for "myApp"`) + ); + + await expect(rulesClient.resolve({ id: '1' })).rejects.toMatchInlineSnapshot( + `[Error: Unauthorized to get a "myType" alert for "myApp"]` + ); + + expect(authorization.ensureAuthorized).toHaveBeenCalledWith({ + entity: 'rule', + consumer: 'myApp', + operation: 'get', + ruleTypeId: 'myType', + }); + }); + }); + + describe('auditLogger', () => { + beforeEach(() => { + unsecuredSavedObjectsClient.resolve.mockResolvedValueOnce({ + saved_object: { + id: '1', + type: 'alert', + attributes: { + alertTypeId: '123', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + actions: [], + }, + references: [], + }, + outcome: 'aliasMatch', + alias_target_id: '2', + }); + }); + + test('logs audit event when getting a rule', async () => { + const rulesClient = new RulesClient({ ...rulesClientParams, auditLogger }); + await rulesClient.resolve({ id: '1' }); + expect(auditLogger.log).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.objectContaining({ + action: 'rule_resolve', + outcome: 'success', + }), + kibana: { saved_object: { id: '1', type: 'alert' } }, + }) + ); + }); + + test('logs audit event when not authorised to get a rule', async () => { + const rulesClient = new RulesClient({ ...rulesClientParams, auditLogger }); + authorization.ensureAuthorized.mockRejectedValue(new Error('Unauthorized')); + + await expect(rulesClient.resolve({ id: '1' })).rejects.toThrow(); + expect(auditLogger.log).toHaveBeenCalledWith( + expect.objectContaining({ + event: expect.objectContaining({ + action: 'rule_resolve', + outcome: 'failure', + }), + kibana: { + saved_object: { + id: '1', + type: 'alert', + }, + }, + error: { + code: 'Error', + message: 'Unauthorized', + }, + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 4033889d9811e..9e9af347af2a2 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -8,7 +8,7 @@ "server": true, "ui": true, "optionalPlugins": ["alerting", "features", "home", "spaces"], - "requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects"], + "requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects", "spacesOss"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], "requiredBundles": ["home", "alerting", "esUiShared", "kibanaReact", "kibanaUtils"] diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index 9786f5dcb949d..cc34f1beaf6b9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -18,6 +18,7 @@ import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerting/public'; import type { SpacesPluginStart } from '../../../spaces/public'; +import type { SpacesOssPluginStart } from '../../../../../src/plugins/spaces_oss/public'; import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; @@ -36,6 +37,7 @@ export interface TriggersAndActionsUiServices extends CoreStart { charts: ChartsPluginStart; alerting?: AlertingStart; spaces?: SpacesPluginStart; + spacesOss: SpacesOssPluginStart; storage?: Storage; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; actionTypeRegistry: ActionTypeRegistryContract; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts index 749cf53cf740b..5049a37c317dd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts @@ -6,7 +6,7 @@ */ import { AlertExecutionStatus } from '../../../../../alerting/common'; import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common'; -import { Alert, AlertAction } from '../../../types'; +import { Alert, AlertAction, ResolvedRule } from '../../../types'; const transformAction: RewriteRequestCase = ({ group, @@ -59,3 +59,16 @@ export const transformAlert: RewriteRequestCase = ({ scheduledTaskId, ...rest, }); + +export const transformResolvedRule: RewriteRequestCase = ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + alias_target_id, + outcome, + ...rest +}: any) => { + return { + ...transformAlert(rest), + alias_target_id, + outcome, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts index a0b090a474e28..c499f7955e2fe 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/index.ts @@ -22,3 +22,4 @@ export { loadAlertState } from './state'; export { unmuteAlertInstance } from './unmute_alert'; export { unmuteAlert, unmuteAlerts } from './unmute'; export { updateAlert } from './update'; +export { resolveRule } from './resolve_rule'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.test.ts new file mode 100644 index 0000000000000..14b64f56f31ff --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { httpServiceMock } from '../../../../../../../src/core/public/mocks'; +import { resolveRule } from './resolve_rule'; +import uuid from 'uuid'; + +const http = httpServiceMock.createStartContract(); + +describe('resolveRule', () => { + test('should call get API with base parameters', async () => { + const ruleId = `${uuid.v4()}/`; + const ruleIdEncoded = encodeURIComponent(ruleId); + const resolvedValue = { + id: '1/', + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'canvas-element.@created', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: ['sdfsdf'], + name: 'dfsdfdsf', + enabled: true, + throttle: '1h', + rule_type_id: '.index-threshold', + created_by: 'elastic', + updated_by: 'elastic', + created_at: '2021-04-01T20:29:18.652Z', + updated_at: '2021-04-01T20:33:38.260Z', + api_key_owner: 'elastic', + notify_when: 'onThrottleInterval', + mute_all: false, + muted_alert_ids: [], + scheduled_task_id: '1', + execution_status: { status: 'ok', last_execution_date: '2021-04-01T21:16:46.709Z' }, + actions: [ + { + group: 'threshold met', + id: '1', + params: { documents: [{ dsfsdf: 1212 }] }, + connector_type_id: '.index', + }, + ], + outcome: 'aliasMatch', + alias_target_id: '2', + }; + http.get.mockResolvedValueOnce(resolvedValue); + + expect(await resolveRule({ http, ruleId })).toEqual({ + id: '1/', + params: { + aggType: 'count', + termSize: 5, + thresholdComparator: '>', + timeWindowSize: 5, + timeWindowUnit: 'm', + groupBy: 'all', + threshold: [1000], + index: ['.kibana'], + timeField: 'canvas-element.@created', + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: ['sdfsdf'], + name: 'dfsdfdsf', + enabled: true, + throttle: '1h', + alertTypeId: '.index-threshold', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: '2021-04-01T20:29:18.652Z', + updatedAt: '2021-04-01T20:33:38.260Z', + apiKeyOwner: 'elastic', + notifyWhen: 'onThrottleInterval', + muteAll: false, + mutedInstanceIds: [], + scheduledTaskId: '1', + executionStatus: { status: 'ok', lastExecutionDate: '2021-04-01T21:16:46.709Z' }, + actions: [ + { + group: 'threshold met', + id: '1', + params: { documents: [{ dsfsdf: 1212 }] }, + actionTypeId: '.index', + }, + ], + outcome: 'aliasMatch', + alias_target_id: '2', + }); + expect(http.get).toHaveBeenCalledWith(`/internal/alerting/rule/${ruleIdEncoded}/_resolve`); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.ts new file mode 100644 index 0000000000000..bc2a19d298f8a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/resolve_rule.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpSetup } from 'kibana/public'; +import { ResolvedRule } from '../../../types'; +import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; +import { transformResolvedRule } from './common_transformations'; + +export async function resolveRule({ + http, + ruleId, +}: { + http: HttpSetup; + ruleId: string; +}): Promise { + const res = await http.get( + `${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(ruleId)}/_resolve` + ); + return transformResolvedRule(res); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx index 41c70a6737fa0..441daad1a50bd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx @@ -8,45 +8,150 @@ import * as React from 'react'; import uuid from 'uuid'; import { shallow } from 'enzyme'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; import { createMemoryHistory, createLocation } from 'history'; import { ToastsApi } from 'kibana/public'; -import { AlertDetailsRoute, getAlertData } from './alert_details_route'; +import { AlertDetailsRoute, getRuleData } from './alert_details_route'; import { Alert } from '../../../../types'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; +import { spacesOssPluginMock } from 'src/plugins/spaces_oss/public/mocks'; +import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); +class NotFoundError extends Error { + public readonly body: { + statusCode: number; + name: string; + } = { + statusCode: 404, + name: 'Not found', + }; + + constructor(message: string | undefined) { + super(message); + } +} + describe('alert_details_route', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const spacesOssMock = spacesOssPluginMock.createStartContract(); + async function setup() { + const useKibanaMock = useKibana as jest.Mocked; + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.spacesOss = spacesOssMock; + } + it('render a loader while fetching data', () => { - const alert = mockAlert(); + const rule = mockRule(); expect( shallow( - + ).containsMatchingElement() ).toBeTruthy(); }); + + it('redirects to another page if fetched rule is an aliasMatch', async () => { + await setup(); + const rule = mockRule(); + const { loadAlert, resolveRule } = mockApis(); + + loadAlert.mockImplementationOnce(async () => { + throw new NotFoundError('OMG'); + }); + resolveRule.mockImplementationOnce(async () => ({ + ...rule, + id: 'new_id', + outcome: 'aliasMatch', + alias_target_id: rule.id, + })); + const wrapper = mountWithIntl( + + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(loadAlert).toHaveBeenCalledWith(rule.id); + expect(resolveRule).toHaveBeenCalledWith(rule.id); + expect((spacesOssMock as any).ui.redirectLegacyUrl).toHaveBeenCalledWith( + `insightsAndAlerting/triggersActions/rule/new_id`, + `rule` + ); + }); + + it('shows warning callout if fetched rule is a conflict', async () => { + await setup(); + const rule = mockRule(); + const ruleType = { + id: rule.alertTypeId, + name: 'type name', + authorizedConsumers: ['consumer'], + }; + const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); + + loadAlert.mockImplementationOnce(async () => { + throw new NotFoundError('OMG'); + }); + loadAlertTypes.mockImplementationOnce(async () => [ruleType]); + loadActionTypes.mockImplementation(async () => []); + resolveRule.mockImplementationOnce(async () => ({ + ...rule, + id: 'new_id', + outcome: 'conflict', + alias_target_id: rule.id, + })); + const wrapper = mountWithIntl( + + ); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(loadAlert).toHaveBeenCalledWith(rule.id); + expect(resolveRule).toHaveBeenCalledWith(rule.id); + expect((spacesOssMock as any).ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ + currentObjectId: 'new_id', + objectNoun: 'rule', + otherObjectId: rule.id, + otherObjectPath: `insightsAndAlerting/triggersActions/rule/${rule.id}`, + }); + }); }); -describe('getAlertData useEffect handler', () => { +describe('getRuleData useEffect handler', () => { beforeEach(() => { jest.clearAllMocks(); }); - it('fetches alert', async () => { - const alert = mockAlert(); - const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + it('fetches rule', async () => { + const rule = mockRule(); + const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); - loadAlert.mockImplementationOnce(async () => alert); + loadAlert.mockImplementationOnce(async () => rule); const toastNotifications = ({ addDanger: jest.fn(), } as unknown) as ToastsApi; - await getAlertData( - alert.id, + await getRuleData( + rule.id, loadAlert, loadAlertTypes, + resolveRule, loadActionTypes, setAlert, setAlertType, @@ -54,45 +159,47 @@ describe('getAlertData useEffect handler', () => { toastNotifications ); - expect(loadAlert).toHaveBeenCalledWith(alert.id); - expect(setAlert).toHaveBeenCalledWith(alert); + expect(loadAlert).toHaveBeenCalledWith(rule.id); + expect(resolveRule).not.toHaveBeenCalled(); + expect(setAlert).toHaveBeenCalledWith(rule); }); - it('fetches alert and action types', async () => { - const actionType = { + it('fetches rule and connector types', async () => { + const connectorType = { id: '.server-log', name: 'Server log', enabled: true, }; - const alert = mockAlert({ + const rule = mockRule({ actions: [ { group: '', id: uuid.v4(), - actionTypeId: actionType.id, + actionTypeId: connectorType.id, params: {}, }, ], }); - const alertType = { - id: alert.alertTypeId, + const ruleType = { + id: rule.alertTypeId, name: 'type name', }; - const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); - loadAlert.mockImplementation(async () => alert); - loadAlertTypes.mockImplementation(async () => [alertType]); - loadActionTypes.mockImplementation(async () => [actionType]); + loadAlert.mockImplementation(async () => rule); + loadAlertTypes.mockImplementation(async () => [ruleType]); + loadActionTypes.mockImplementation(async () => [connectorType]); const toastNotifications = ({ addDanger: jest.fn(), } as unknown) as ToastsApi; - await getAlertData( - alert.id, + await getRuleData( + rule.id, loadAlert, loadAlertTypes, + resolveRule, loadActionTypes, setAlert, setAlertType, @@ -102,29 +209,76 @@ describe('getAlertData useEffect handler', () => { expect(loadAlertTypes).toHaveBeenCalledTimes(1); expect(loadActionTypes).toHaveBeenCalledTimes(1); + expect(resolveRule).not.toHaveBeenCalled(); + + expect(setAlert).toHaveBeenCalledWith(rule); + expect(setAlertType).toHaveBeenCalledWith(ruleType); + expect(setActionTypes).toHaveBeenCalledWith([connectorType]); + }); + + it('fetches rule using resolve if initial GET results in a 404 error', async () => { + const connectorType = { + id: '.server-log', + name: 'Server log', + enabled: true, + }; + const rule = mockRule({ + actions: [ + { + group: '', + id: uuid.v4(), + actionTypeId: connectorType.id, + params: {}, + }, + ], + }); + + const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); + const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); + + loadAlert.mockImplementationOnce(async () => { + throw new NotFoundError('OMG'); + }); + resolveRule.mockImplementationOnce(async () => rule); + + const toastNotifications = ({ + addDanger: jest.fn(), + } as unknown) as ToastsApi; + await getRuleData( + rule.id, + loadAlert, + loadAlertTypes, + resolveRule, + loadActionTypes, + setAlert, + setAlertType, + setActionTypes, + toastNotifications + ); - expect(setAlertType).toHaveBeenCalledWith(alertType); - expect(setActionTypes).toHaveBeenCalledWith([actionType]); + expect(loadAlert).toHaveBeenCalledWith(rule.id); + expect(resolveRule).toHaveBeenCalledWith(rule.id); + expect(setAlert).toHaveBeenCalledWith(rule); }); - it('displays an error if the alert isnt found', async () => { - const actionType = { + it('displays an error if fetching the rule results in a non-404 error', async () => { + const connectorType = { id: '.server-log', name: 'Server log', enabled: true, }; - const alert = mockAlert({ + const rule = mockRule({ actions: [ { group: '', id: uuid.v4(), - actionTypeId: actionType.id, + actionTypeId: connectorType.id, params: {}, }, ], }); - const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); loadAlert.mockImplementation(async () => { @@ -134,10 +288,11 @@ describe('getAlertData useEffect handler', () => { const toastNotifications = ({ addDanger: jest.fn(), } as unknown) as ToastsApi; - await getAlertData( - alert.id, + await getRuleData( + rule.id, loadAlert, loadAlertTypes, + resolveRule, loadActionTypes, setAlert, setAlertType, @@ -150,40 +305,41 @@ describe('getAlertData useEffect handler', () => { }); }); - it('displays an error if the alert type isnt loaded', async () => { - const actionType = { + it('displays an error if the rule type isnt loaded', async () => { + const connectorType = { id: '.server-log', name: 'Server log', enabled: true, }; - const alert = mockAlert({ + const rule = mockRule({ actions: [ { group: '', id: uuid.v4(), - actionTypeId: actionType.id, + actionTypeId: connectorType.id, params: {}, }, ], }); - const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); - loadAlert.mockImplementation(async () => alert); + loadAlert.mockImplementation(async () => rule); loadAlertTypes.mockImplementation(async () => { - throw new Error('OMG no alert type'); + throw new Error('OMG no rule type'); }); - loadActionTypes.mockImplementation(async () => [actionType]); + loadActionTypes.mockImplementation(async () => [connectorType]); const toastNotifications = ({ addDanger: jest.fn(), } as unknown) as ToastsApi; - await getAlertData( - alert.id, + await getRuleData( + rule.id, loadAlert, loadAlertTypes, + resolveRule, loadActionTypes, setAlert, setAlertType, @@ -192,48 +348,49 @@ describe('getAlertData useEffect handler', () => { ); expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); expect(toastNotifications.addDanger).toHaveBeenCalledWith({ - title: 'Unable to load rule: OMG no alert type', + title: 'Unable to load rule: OMG no rule type', }); }); - it('displays an error if the action type isnt loaded', async () => { - const actionType = { + it('displays an error if the connector type isnt loaded', async () => { + const connectorType = { id: '.server-log', name: 'Server log', enabled: true, }; - const alert = mockAlert({ + const rule = mockRule({ actions: [ { group: '', id: uuid.v4(), - actionTypeId: actionType.id, + actionTypeId: connectorType.id, params: {}, }, ], }); - const alertType = { - id: alert.alertTypeId, + const ruleType = { + id: rule.alertTypeId, name: 'type name', }; - const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); - loadAlert.mockImplementation(async () => alert); + loadAlert.mockImplementation(async () => rule); - loadAlertTypes.mockImplementation(async () => [alertType]); + loadAlertTypes.mockImplementation(async () => [ruleType]); loadActionTypes.mockImplementation(async () => { - throw new Error('OMG no action type'); + throw new Error('OMG no connector type'); }); const toastNotifications = ({ addDanger: jest.fn(), } as unknown) as ToastsApi; - await getAlertData( - alert.id, + await getRuleData( + rule.id, loadAlert, loadAlertTypes, + resolveRule, loadActionTypes, setAlert, setAlertType, @@ -242,46 +399,47 @@ describe('getAlertData useEffect handler', () => { ); expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); expect(toastNotifications.addDanger).toHaveBeenCalledWith({ - title: 'Unable to load rule: OMG no action type', + title: 'Unable to load rule: OMG no connector type', }); }); - it('displays an error if the alert type isnt found', async () => { - const actionType = { + it('displays an error if the rule type isnt found', async () => { + const connectorType = { id: '.server-log', name: 'Server log', enabled: true, }; - const alert = mockAlert({ + const rule = mockRule({ actions: [ { group: '', id: uuid.v4(), - actionTypeId: actionType.id, + actionTypeId: connectorType.id, params: {}, }, ], }); - const alertType = { + const ruleType = { id: uuid.v4(), name: 'type name', }; - const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); - loadAlert.mockImplementation(async () => alert); - loadAlertTypes.mockImplementation(async () => [alertType]); - loadActionTypes.mockImplementation(async () => [actionType]); + loadAlert.mockImplementation(async () => rule); + loadAlertTypes.mockImplementation(async () => [ruleType]); + loadActionTypes.mockImplementation(async () => [connectorType]); const toastNotifications = ({ addDanger: jest.fn(), } as unknown) as ToastsApi; - await getAlertData( - alert.id, + await getRuleData( + rule.id, loadAlert, loadAlertTypes, + resolveRule, loadActionTypes, setAlert, setAlertType, @@ -290,57 +448,58 @@ describe('getAlertData useEffect handler', () => { ); expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); expect(toastNotifications.addDanger).toHaveBeenCalledWith({ - title: `Unable to load rule: Invalid Alert Type: ${alert.alertTypeId}`, + title: `Unable to load rule: Invalid Rule Type: ${rule.alertTypeId}`, }); }); it('displays an error if an action type isnt found', async () => { - const availableActionType = { + const availableConnectorType = { id: '.server-log', name: 'Server log', enabled: true, }; - const missingActionType = { + const missingConnectorType = { id: '.noop', name: 'No Op', enabled: true, }; - const alert = mockAlert({ + const rule = mockRule({ actions: [ { group: '', id: uuid.v4(), - actionTypeId: availableActionType.id, + actionTypeId: availableConnectorType.id, params: {}, }, { group: '', id: uuid.v4(), - actionTypeId: missingActionType.id, + actionTypeId: missingConnectorType.id, params: {}, }, ], }); - const alertType = { + const ruleType = { id: uuid.v4(), name: 'type name', }; - const { loadAlert, loadAlertTypes, loadActionTypes } = mockApis(); + const { loadAlert, loadAlertTypes, loadActionTypes, resolveRule } = mockApis(); const { setAlert, setAlertType, setActionTypes } = mockStateSetter(); - loadAlert.mockImplementation(async () => alert); - loadAlertTypes.mockImplementation(async () => [alertType]); - loadActionTypes.mockImplementation(async () => [availableActionType]); + loadAlert.mockImplementation(async () => rule); + loadAlertTypes.mockImplementation(async () => [ruleType]); + loadActionTypes.mockImplementation(async () => [availableConnectorType]); const toastNotifications = ({ addDanger: jest.fn(), } as unknown) as ToastsApi; - await getAlertData( - alert.id, + await getRuleData( + rule.id, loadAlert, loadAlertTypes, + resolveRule, loadActionTypes, setAlert, setAlertType, @@ -349,7 +508,7 @@ describe('getAlertData useEffect handler', () => { ); expect(toastNotifications.addDanger).toHaveBeenCalledTimes(1); expect(toastNotifications.addDanger).toHaveBeenCalledWith({ - title: `Unable to load rule: Invalid Action Type: ${missingActionType.id}`, + title: `Unable to load rule: Invalid Connector Type: ${missingConnectorType.id}`, }); }); }); @@ -359,6 +518,7 @@ function mockApis() { loadAlert: jest.fn(), loadAlertTypes: jest.fn(), loadActionTypes: jest.fn(), + resolveRule: jest.fn(), }; } @@ -370,23 +530,23 @@ function mockStateSetter() { }; } -function mockRouterProps(alert: Alert) { +function mockRouterProps(rule: Alert) { return { match: { isExact: false, - path: `/rule/${alert.id}`, + path: `/rule/${rule.id}`, url: '', - params: { ruleId: alert.id }, + params: { ruleId: rule.id }, }, history: createMemoryHistory(), - location: createLocation(`/rule/${alert.id}`), + location: createLocation(`/rule/${rule.id}`), }; } -function mockAlert(overloads: Partial = {}): Alert { +function mockRule(overloads: Partial = {}): Alert { return { id: uuid.v4(), enabled: true, - name: `alert-${uuid.v4()}`, + name: `rule-${uuid.v4()}`, tags: [], alertTypeId: '.noop', consumer: 'consumer', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx index 2d6db5f6330cc..b6279d7fca100 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx @@ -9,7 +9,8 @@ import { i18n } from '@kbn/i18n'; import React, { useState, useEffect } from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { ToastsApi } from 'kibana/public'; -import { Alert, AlertType, ActionType } from '../../../../types'; +import { EuiSpacer } from '@elastic/eui'; +import { Alert, AlertType, ActionType, ResolvedRule } from '../../../../types'; import { AlertDetailsWithApi as AlertDetails } from './alert_details'; import { throwIfAbsent, throwIfIsntContained } from '../../../lib/value_validators'; import { @@ -27,7 +28,7 @@ type AlertDetailsRouteProps = RouteComponentProps<{ ruleId: string; }> & Pick & - Pick; + Pick; export const AlertDetailsRoute: React.FunctionComponent = ({ match: { @@ -36,63 +37,127 @@ export const AlertDetailsRoute: React.FunctionComponent loadAlert, loadAlertTypes, loadActionTypes, + resolveRule, }) => { const { http, notifications: { toasts }, + spacesOss, } = useKibana().services; - const [alert, setAlert] = useState(null); + const { basePath } = http; + + const [alert, setAlert] = useState(null); const [alertType, setAlertType] = useState(null); const [actionTypes, setActionTypes] = useState(null); const [refreshToken, requestRefresh] = React.useState(); useEffect(() => { - getAlertData( + getRuleData( ruleId, loadAlert, loadAlertTypes, + resolveRule, loadActionTypes, setAlert, setAlertType, setActionTypes, toasts ); - }, [ruleId, http, loadActionTypes, loadAlert, loadAlertTypes, toasts, refreshToken]); + }, [ruleId, http, loadActionTypes, loadAlert, loadAlertTypes, resolveRule, toasts, refreshToken]); + + useEffect(() => { + if (alert) { + const outcome = (alert as ResolvedRule).outcome; + if (outcome === 'aliasMatch' && spacesOss.isSpacesAvailable) { + // This rule has been resolved from a legacy URL - redirect the user to the new URL and display a toast. + const path = basePath.prepend(`insightsAndAlerting/triggersActions/rule/${alert.id}`); + spacesOss.ui.redirectLegacyUrl( + path, + i18n.translate('xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', { + defaultMessage: 'rule', + }) + ); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [alert]); + + const getLegacyUrlConflictCallout = () => { + const outcome = (alert as ResolvedRule).outcome; + const aliasTargetId = (alert as ResolvedRule).alias_target_id; + if (outcome === 'conflict' && aliasTargetId && spacesOss.isSpacesAvailable) { + // We have resolved to one rule, but there is another one with a legacy URL associated with this page. Display a + // callout with a warning for the user, and provide a way for them to navigate to the other rule. + const otherRulePath = basePath.prepend( + `insightsAndAlerting/triggersActions/rule/${aliasTargetId}` + ); + return ( + <> + + {spacesOss.ui.components.getLegacyUrlConflict({ + objectNoun: i18n.translate( + 'xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', + { + defaultMessage: 'rule', + } + ), + currentObjectId: alert?.id!, + otherObjectId: aliasTargetId, + otherObjectPath: otherRulePath, + })} + + ); + } + return null; + }; return alert && alertType && actionTypes ? ( - requestRefresh(Date.now())} - /> + <> + {getLegacyUrlConflictCallout()} + requestRefresh(Date.now())} + /> + ) : ( ); }; -export async function getAlertData( - alertId: string, +export async function getRuleData( + ruleId: string, loadAlert: AlertApis['loadAlert'], loadAlertTypes: AlertApis['loadAlertTypes'], + resolveRule: AlertApis['resolveRule'], loadActionTypes: ActionApis['loadActionTypes'], - setAlert: React.Dispatch>, + setAlert: React.Dispatch>, setAlertType: React.Dispatch>, setActionTypes: React.Dispatch>, toasts: Pick ) { try { - const loadedAlert = await loadAlert(alertId); - setAlert(loadedAlert); + let loadedRule: Alert | ResolvedRule; + try { + loadedRule = await loadAlert(ruleId); + } catch (err) { + // Try resolving this rule id if the error is a 404, otherwise re-throw + if (err?.body?.statusCode !== 404) { + throw err; + } + loadedRule = await resolveRule(ruleId); + } + setAlert(loadedRule); const [loadedAlertType, loadedActionTypes] = await Promise.all([ loadAlertTypes() - .then((types) => types.find((type) => type.id === loadedAlert.alertTypeId)) - .then(throwIfAbsent(`Invalid Alert Type: ${loadedAlert.alertTypeId}`)), + .then((types) => types.find((type) => type.id === loadedRule.alertTypeId)) + .then(throwIfAbsent(`Invalid Rule Type: ${loadedRule.alertTypeId}`)), loadActionTypes().then( throwIfIsntContained( - new Set(loadedAlert.actions.map((action) => action.actionTypeId)), - (requiredActionType: string) => `Invalid Action Type: ${requiredActionType}`, + new Set(loadedRule.actions.map((action) => action.actionTypeId)), + (requiredActionType: string) => `Invalid Connector Type: ${requiredActionType}`, (action: ActionType) => action.id ) ), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx index 7d314cef55680..806f649e7d033 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.test.tsx @@ -36,6 +36,7 @@ describe('with_bulk_alert_api_operations', () => { expect(typeof props.deleteAlert).toEqual('function'); expect(typeof props.loadAlert).toEqual('function'); expect(typeof props.loadAlertTypes).toEqual('function'); + expect(typeof props.resolveRule).toEqual('function'); return
; }; @@ -220,6 +221,24 @@ describe('with_bulk_alert_api_operations', () => { expect(alertApi.loadAlert).toHaveBeenCalledWith({ alertId, http }); }); + it('resolveRule calls the resolveRule api', () => { + const { http } = useKibanaMock().services; + const ComponentToExtend = ({ + resolveRule, + ruleId, + }: ComponentOpts & { ruleId: Alert['id'] }) => { + return ; + }; + + const ExtendedComponent = withBulkAlertOperations(ComponentToExtend); + const ruleId = uuid.v4(); + const component = mount(); + component.find('button').simulate('click'); + + expect(alertApi.resolveRule).toHaveBeenCalledTimes(1); + expect(alertApi.resolveRule).toHaveBeenCalledWith({ ruleId, http }); + }); + it('loadAlertTypes calls the loadAlertTypes api', () => { const { http } = useKibanaMock().services; const ComponentToExtend = ({ loadAlertTypes }: ComponentOpts) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx index 983fe5641e62b..59919c202277c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/with_bulk_alert_api_operations.tsx @@ -13,6 +13,7 @@ import { AlertTaskState, AlertInstanceSummary, AlertingFrameworkHealth, + ResolvedRule, } from '../../../../types'; import { deleteAlerts, @@ -31,6 +32,7 @@ import { loadAlertInstanceSummary, loadAlertTypes, alertingFrameworkHealth, + resolveRule, } from '../../../lib/alert_api'; import { useKibana } from '../../../../common/lib/kibana'; @@ -62,6 +64,7 @@ export interface ComponentOpts { loadAlertInstanceSummary: (id: Alert['id']) => Promise; loadAlertTypes: () => Promise; getHealth: () => Promise; + resolveRule: (id: Alert['id']) => Promise; } export type PropsWithOptionalApiHandlers = Omit & Partial; @@ -132,6 +135,7 @@ export function withBulkAlertOperations( loadAlertInstanceSummary({ http, alertId }) } loadAlertTypes={async () => loadAlertTypes({ http })} + resolveRule={async (ruleId: Alert['id']) => resolveRule({ http, ruleId })} getHealth={async () => alertingFrameworkHealth({ http })} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts index a1a0184198dfd..f8aa483711c30 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts @@ -8,7 +8,7 @@ import React from 'react'; import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; - +import { spacesOssPluginMock } from '../../../../../../../src/plugins/spaces_oss/public/mocks'; import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { TriggersAndActionsUiServices } from '../../../application/app'; @@ -45,6 +45,7 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => { element: ({ style: { cursor: 'pointer' }, } as unknown) as HTMLElement, + spacesOss: spacesOssPluginMock.createStartContract(), } as TriggersAndActionsUiServices; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 7661eefba7f65..36d6964ce7753 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -26,6 +26,7 @@ import { PluginStartContract as AlertingStart } from '../../alerting/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; import type { SpacesPluginStart } from '../../spaces/public'; +import type { SpacesOssPluginStart } from '../../../../src/plugins/spaces_oss/public'; import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout'; import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; @@ -73,6 +74,7 @@ interface PluginsStart { charts: ChartsPluginStart; alerting?: AlertingStart; spaces?: SpacesPluginStart; + spacesOss: SpacesOssPluginStart; navigateToApp: CoreStart['application']['navigateToApp']; features: FeaturesPluginStart; } @@ -148,6 +150,7 @@ export class Plugin charts: pluginsStart.charts, alerting: pluginsStart.alerting, spaces: pluginsStart.spaces, + spacesOss: pluginsStart.spacesOss, element: params.element, storage: new Storage(window.localStorage), setBreadcrumbs: params.setBreadcrumbs, diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index f01967592ea8c..ae4fd5152794f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -24,6 +24,7 @@ import { ActionGroup, AlertActionParam, SanitizedAlert, + ResolvedSanitizedRule, AlertAction, AlertAggregations, AlertTaskState, @@ -40,6 +41,7 @@ import { // In Triggers and Actions we treat all `Alert`s as `SanitizedAlert` // so the `Params` is a black-box of Record type Alert = SanitizedAlert; +type ResolvedRule = ResolvedSanitizedRule; export { Alert, @@ -52,6 +54,7 @@ export { AlertingFrameworkHealth, AlertNotifyWhenType, AlertTypeParams, + ResolvedRule, }; export { ActionType, diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index 6536206acf369..e3c8b77d2c1d4 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -24,5 +24,6 @@ { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/management/tsconfig.json" }, + { "path": "../../../src/plugins/spaces_oss/tsconfig.json" }, ] } From 4d0623ee25b288609742ad6fd5e5776c0f71ccce Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Thu, 19 Aug 2021 00:28:52 -0500 Subject: [PATCH 16/28] [ML] Add tooltip help text for Transform filter query control (#108720) * [ML] Add tooltip help text * [ML] Remove unused import * [ML] Update tooltip content to be clearer Co-authored-by: Lisa Cawley Co-authored-by: Lisa Cawley Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../filter_agg/components/filter_agg_form.tsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx index e3e767a81b01d..543ecc7dee195 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_agg_form.tsx @@ -6,7 +6,7 @@ */ import React, { useContext, useMemo } from 'react'; -import { EuiFormRow, EuiSelect } from '@elastic/eui'; +import { EuiFormRow, EuiIcon, EuiSelect, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import { CreateTransformWizardContext } from '../../../../wizard/wizard'; @@ -72,10 +72,22 @@ export const FilterAggForm: PivotAggsConfigFilter['AggFormComponent'] = ({ <> + <> + + + } + > + + + } > Date: Thu, 19 Aug 2021 09:04:03 +0300 Subject: [PATCH 17/28] Fixed flakiness with visualize custom role (#109077) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apps/visualize/feature_controls/visualize_security.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index 2994e18fa9ab7..7bfae9ba36be4 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -32,6 +32,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/visualize/default'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional'); + // ensure we're logged out so we can login as the appropriate users + await PageObjects.security.forceLogout(); }); after(async () => { @@ -81,7 +83,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Visualize Library', 'Stack Management']); + expect(navLinks).to.contain('Visualize Library'); }); it(`landing page shows "Create new Visualization" button`, async () => { From 2172b88daef63777b20b282e74f394a372129453 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 19 Aug 2021 09:04:53 +0300 Subject: [PATCH 18/28] Fix flakiness in load and save agg-based visualizations (#109066) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/page_objects/visualize_page.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/functional/page_objects/visualize_page.ts b/test/functional/page_objects/visualize_page.ts index 1271fe5108f56..cf3a692d1622e 100644 --- a/test/functional/page_objects/visualize_page.ts +++ b/test/functional/page_objects/visualize_page.ts @@ -310,6 +310,7 @@ export class VisualizePageObject extends FtrService { if (navigateToVisualize) { await this.clickLoadSavedVisButton(); } + await this.listingTable.searchForItemWithName(vizName); await this.openSavedVisualization(vizName); } From 544c41e21418dd5e3ff73c9e879f035594b1f359 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 19 Aug 2021 08:18:28 +0200 Subject: [PATCH 19/28] [Discover] Unskip discover tab field data functional tests (#107305) --- test/functional/apps/discover/_data_grid.ts | 4 +-- .../apps/discover/_data_grid_doc_table.ts | 2 +- test/functional/apps/discover/_discover.ts | 2 +- test/functional/apps/discover/_doc_table.ts | 2 +- test/functional/apps/discover/_field_data.ts | 3 +- .../discover/_field_data_with_fields_api.ts | 3 +- test/functional/page_objects/discover_page.ts | 30 ++++++++++++++++--- 7 files changed, 33 insertions(+), 13 deletions(-) diff --git a/test/functional/apps/discover/_data_grid.ts b/test/functional/apps/discover/_data_grid.ts index efd97fce3f7f5..4a343fb30384e 100644 --- a/test/functional/apps/discover/_data_grid.ts +++ b/test/functional/apps/discover/_data_grid.ts @@ -47,10 +47,10 @@ export default function ({ await PageObjects.discover.clickFieldListItemAdd('agent'); expect(await getTitles()).to.be('Time (@timestamp) bytes agent'); - await PageObjects.discover.clickFieldListItemAdd('bytes'); + await PageObjects.discover.clickFieldListItemRemove('bytes'); expect(await getTitles()).to.be('Time (@timestamp) agent'); - await PageObjects.discover.clickFieldListItemAdd('agent'); + await PageObjects.discover.clickFieldListItemRemove('agent'); expect(await getTitles()).to.be('Time (@timestamp) Document'); }); }); diff --git a/test/functional/apps/discover/_data_grid_doc_table.ts b/test/functional/apps/discover/_data_grid_doc_table.ts index 00e6a5025e2af..2efb1ba51811f 100644 --- a/test/functional/apps/discover/_data_grid_doc_table.ts +++ b/test/functional/apps/discover/_data_grid_doc_table.ts @@ -161,7 +161,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); } // remove the second column - await PageObjects.discover.clickFieldListItemAdd(extraColumns[1]); + await PageObjects.discover.clickFieldListItemRemove(extraColumns[1]); await PageObjects.header.waitUntilLoadingHasFinished(); // test that the second column is no longer there const header = await dataGrid.getHeaderFields(); diff --git a/test/functional/apps/discover/_discover.ts b/test/functional/apps/discover/_discover.ts index 8dc695abc8d45..4704f75f34289 100644 --- a/test/functional/apps/discover/_discover.ts +++ b/test/functional/apps/discover/_discover.ts @@ -310,7 +310,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.clickFieldSort('_score', 'Sort Low-High'); const currentUrlWithScore = await browser.getCurrentUrl(); expect(currentUrlWithScore).to.contain('_score'); - await PageObjects.discover.clickFieldListItemAdd('_score'); + await PageObjects.discover.clickFieldListItemRemove('_score'); const currentUrlWithoutScore = await browser.getCurrentUrl(); expect(currentUrlWithoutScore).not.to.contain('_score'); }); diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index 09a162e051bf6..f01d6b18fbf01 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -218,7 +218,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); } // remove the second column - await PageObjects.discover.clickFieldListItemAdd(extraColumns[1]); + await PageObjects.discover.clickFieldListItemRemove(extraColumns[1]); await PageObjects.header.waitUntilLoadingHasFinished(); // test that the second column is no longer there const docHeader = await find.byCssSelector('thead > tr:nth-child(1)'); diff --git a/test/functional/apps/discover/_field_data.ts b/test/functional/apps/discover/_field_data.ts index fc0c0c6a48649..27407e9a0bc4d 100644 --- a/test/functional/apps/discover/_field_data.ts +++ b/test/functional/apps/discover/_field_data.ts @@ -34,8 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); - // FLAKY: https://github.com/elastic/kibana/issues/100437 - describe.skip('field data', function () { + describe('field data', function () { it('search php should show the correct hit count', async function () { const expectedHitCount = '445'; await retry.try(async function () { diff --git a/test/functional/apps/discover/_field_data_with_fields_api.ts b/test/functional/apps/discover/_field_data_with_fields_api.ts index 97c1e678c4a9f..666377ae7f794 100644 --- a/test/functional/apps/discover/_field_data_with_fields_api.ts +++ b/test/functional/apps/discover/_field_data_with_fields_api.ts @@ -34,8 +34,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRangeViaUiSettings(); await PageObjects.common.navigateToApp('discover'); }); - // FLAKY: https://github.com/elastic/kibana/issues/103389 - describe.skip('field data', function () { + describe('field data', function () { it('search php should show the correct hit count', async function () { const expectedHitCount = '445'; await retry.try(async function () { diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index a4d8f884e1824..ae1b4fbf3179a 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -353,17 +353,39 @@ export class DiscoverPageObject extends FtrService { public async clickFieldListItemAdd(field: string) { // a filter check may make sense here, but it should be properly handled to make // it work with the _score and _source fields as well + if (await this.isFieldSelected(field)) { + return; + } await this.clickFieldListItemToggle(field); + const isLegacyDefault = await this.useLegacyTable(); + if (isLegacyDefault) { + await this.retry.waitFor(`field ${field} to be added to classic table`, async () => { + return await this.testSubjects.exists(`docTableHeader-${field}`); + }); + } else { + await this.retry.waitFor(`field ${field} to be added to new table`, async () => { + return await this.testSubjects.exists(`dataGridHeaderCell-${field}`); + }); + } } - public async clickFieldListItemRemove(field: string) { + public async isFieldSelected(field: string) { if (!(await this.testSubjects.exists('fieldList-selected'))) { - return; + return false; } const selectedList = await this.testSubjects.find('fieldList-selected'); - if (await this.testSubjects.descendantExists(`field-${field}`, selectedList)) { - await this.clickFieldListItemToggle(field); + return await this.testSubjects.descendantExists(`field-${field}`, selectedList); + } + + public async clickFieldListItemRemove(field: string) { + if ( + !(await this.testSubjects.exists('fieldList-selected')) || + !(await this.isFieldSelected(field)) + ) { + return; } + + await this.clickFieldListItemToggle(field); } public async clickFieldListItemVisualize(fieldName: string) { From 98a3916e839e3e9f17be80751a5a6f5ea43d4932 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 19 Aug 2021 08:41:34 +0200 Subject: [PATCH 20/28] fix tsref? bis --- x-pack/plugins/maps/tsconfig.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index 5245b374b9fec..9589728f2a0f6 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -16,9 +16,27 @@ "references": [ { "path": "../../../src/core/tsconfig.json" }, { "path": "../../../src/plugins/maps_ems/tsconfig.json" }, + { "path": "../../../src/plugins/dashboard/tsconfig.json" }, + { "path": "../../../src/plugins/inspector/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../../../src/plugins/ui_actions/tsconfig.json" }, + { "path": "../../../src/plugins/navigation/tsconfig.json" }, + { "path": "../../../src/plugins/expressions/tsconfig.json" }, + { "path": "../../../src/plugins/visualizations/tsconfig.json" }, + { "path": "../../../src/plugins/embeddable/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json" }, + { "path": "../../../src/plugins/share/tsconfig.json" }, + { "path": "../../../src/plugins/presentation_util/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + { "path": "../../../src/plugins/charts/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, { "path": "../file_upload/tsconfig.json" }, { "path": "../saved_objects_tagging/tsconfig.json" }, + { "path": "../security/tsconfig.json" } ] } From fe08d0aa21fef7f9b7bb7d4de0c57c225f271a67 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 19 Aug 2021 11:27:18 +0300 Subject: [PATCH 21/28] [TSVB] Long legend values support (#108023) * [TSVB] Supports legends with long values * Add a unit test * Design optimization * Revert changes * Add the missing prop type * Ensure that the limits are respected Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/types/panel_model.ts | 2 + .../panel_config/timeseries.test.tsx | 82 ++++++++++++ .../components/panel_config/timeseries.tsx | 122 ++++++++++++------ .../components/vis_types/timeseries/vis.js | 2 + .../visualizations/views/timeseries/index.js | 10 ++ .../public/metrics_type.ts | 2 + x-pack/yarn.lock | 27 ---- 7 files changed, 184 insertions(+), 63 deletions(-) create mode 100644 src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.test.tsx diff --git a/src/plugins/vis_type_timeseries/common/types/panel_model.ts b/src/plugins/vis_type_timeseries/common/types/panel_model.ts index 2ac9125534ac7..ff942a30abbdc 100644 --- a/src/plugins/vis_type_timeseries/common/types/panel_model.ts +++ b/src/plugins/vis_type_timeseries/common/types/panel_model.ts @@ -161,6 +161,8 @@ export interface Panel { series: Series[]; show_grid: number; show_legend: number; + truncate_legend?: number; + max_lines_legend?: number; time_field?: string; time_range_mode?: string; tooltip_mode?: TOOLTIP_MODES; diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.test.tsx new file mode 100644 index 0000000000000..02f28f3135880 --- /dev/null +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.test.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallowWithIntl as shallow } from '@kbn/test/jest'; + +jest.mock('../lib/get_default_query_language', () => ({ + getDefaultQueryLanguage: () => 'kuery', +})); + +import { TimeseriesPanelConfig } from './timeseries'; +import { PanelConfigProps } from './types'; + +describe('TimeseriesPanelConfig', () => { + it('sets the number input to the given value', () => { + const props = ({ + fields: {}, + model: { + max_lines_legend: 2, + }, + onChange: jest.fn(), + } as unknown) as PanelConfigProps; + const wrapper = shallow(); + wrapper.instance().setState({ selectedTab: 'options' }); + expect( + wrapper.find('[data-test-subj="timeSeriesEditorDataMaxLegendLines"]').prop('value') + ).toEqual(2); + }); + + it('switches on the truncate legend switch if the prop is set to 1 ', () => { + const props = ({ + fields: {}, + model: { + max_lines_legend: 2, + truncate_legend: 1, + }, + onChange: jest.fn(), + } as unknown) as PanelConfigProps; + const wrapper = shallow(); + wrapper.instance().setState({ selectedTab: 'options' }); + expect( + wrapper.find('[data-test-subj="timeSeriesEditorDataTruncateLegendSwitch"]').prop('value') + ).toEqual(1); + }); + + it('switches off the truncate legend switch if the prop is set to 0', () => { + const props = ({ + fields: {}, + model: { + max_lines_legend: 2, + truncate_legend: 0, + }, + onChange: jest.fn(), + } as unknown) as PanelConfigProps; + const wrapper = shallow(); + wrapper.instance().setState({ selectedTab: 'options' }); + expect( + wrapper.find('[data-test-subj="timeSeriesEditorDataTruncateLegendSwitch"]').prop('value') + ).toEqual(0); + }); + + it('disables the max lines number input if the truncate legend switch is off', () => { + const props = ({ + fields: {}, + model: { + max_lines_legend: 2, + truncate_legend: 0, + }, + onChange: jest.fn(), + } as unknown) as PanelConfigProps; + const wrapper = shallow(); + wrapper.instance().setState({ selectedTab: 'options' }); + expect( + wrapper.find('[data-test-subj="timeSeriesEditorDataMaxLegendLines"]').prop('disabled') + ).toEqual(true); + }); +}); diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx index cdad8c1aeff4b..25e6c7906d831 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/timeseries.tsx @@ -23,6 +23,7 @@ import { EuiFieldText, EuiTitle, EuiHorizontalRule, + EuiFieldNumber, } from '@elastic/eui'; // @ts-expect-error not typed yet @@ -102,6 +103,9 @@ const legendPositionOptions = [ }, ]; +const MAX_TRUNCATE_LINES = 5; +const MIN_TRUNCATE_LINES = 1; + export class TimeseriesPanelConfig extends Component< PanelConfigProps, { selectedTab: PANEL_CONFIG_TABS } @@ -344,7 +348,7 @@ export class TimeseriesPanelConfig extends Component< /> - + - - + - + - + + + + - + + + - - - + + + + + + - + + + + + + + + + + { + const val = Number(e.target.value); + this.props.onChange({ + max_lines_legend: Math.min( + MAX_TRUNCATE_LINES, + Math.max(val, MIN_TRUNCATE_LINES) + ), + }); + }} + /> + + + + + + + diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index 097b0a7b5e332..d9440804701b2 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -238,6 +238,8 @@ class TimeseriesVisualization extends Component { showGrid={Boolean(model.show_grid)} legend={Boolean(model.show_legend)} legendPosition={model.legend_position} + truncateLegend={Boolean(model.truncate_legend)} + maxLegendLines={model.max_lines_legend} tooltipMode={model.tooltip_mode} xAxisFormatter={this.xAxisFormatter(interval)} annotations={this.prepareAnnotations()} diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js index a818d1d5843de..b470352eec56a 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/timeseries/index.js @@ -56,6 +56,8 @@ export const TimeSeries = ({ showGrid, legend, legendPosition, + truncateLegend, + maxLegendLines, tooltipMode, series, yAxis, @@ -172,6 +174,9 @@ export const TimeSeries = ({ background: { color: backgroundColor, }, + legend: { + labelOptions: { maxLines: truncateLegend ? maxLegendLines ?? 1 : 0 }, + }, }, chartTheme, ]} @@ -216,6 +221,7 @@ export const TimeSeries = ({ lines, data, hideInLegend, + truncateLegend, xScaleType, yScaleType, groupId, @@ -249,6 +255,7 @@ export const TimeSeries = ({ name={getValueOrEmpty(seriesName)} data={data} hideInLegend={hideInLegend} + truncateLegend={truncateLegend} bars={bars} color={finalColor} stackAccessors={stackAccessors} @@ -274,6 +281,7 @@ export const TimeSeries = ({ name={getValueOrEmpty(seriesName)} data={data} hideInLegend={hideInLegend} + truncateLegend={truncateLegend} lines={lines} color={finalColor} stackAccessors={stackAccessors} @@ -336,6 +344,8 @@ TimeSeries.propTypes = { showGrid: PropTypes.bool, legend: PropTypes.bool, legendPosition: PropTypes.string, + truncateLegend: PropTypes.bool, + maxLegendLines: PropTypes.number, series: PropTypes.array, yAxis: PropTypes.array, onBrush: PropTypes.func, diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index d639604c7cd29..b68812b9828e3 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -93,6 +93,8 @@ export const metricsVisDefinition: VisTypeDefinition< axis_formatter: 'number', axis_scale: 'normal', show_legend: 1, + truncate_legend: 1, + max_lines_legend: 1, show_grid: 1, tooltip_mode: TOOLTIP_MODES.SHOW_ALL, drop_last_bucket: 0, diff --git a/x-pack/yarn.lock b/x-pack/yarn.lock index def2ba279bfff..fb57ccd13afbd 100644 --- a/x-pack/yarn.lock +++ b/x-pack/yarn.lock @@ -2,30 +2,3 @@ # yarn lockfile v1 -"@kbn/interpreter@link:../packages/kbn-interpreter": - version "0.0.0" - uid "" - -"@kbn/optimizer@link:../packages/kbn-optimizer": - version "0.0.0" - uid "" - -"@kbn/plugin-helpers@link:../packages/kbn-plugin-helpers": - version "0.0.0" - uid "" - -"@kbn/storybook@link:../packages/kbn-storybook": - version "0.0.0" - uid "" - -"@kbn/test@link:../packages/kbn-test": - version "0.0.0" - uid "" - -"@kbn/ui-framework@link:../packages/kbn-ui-framework": - version "0.0.0" - uid "" - -"@kbn/ui-shared-deps@link:../packages/kbn-ui-shared-deps": - version "0.0.0" - uid "" From cb0ce593765eb71419f343d987aa4d09c0ffe77b Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Thu, 19 Aug 2021 11:24:32 +0200 Subject: [PATCH 22/28] Introduce `Enroll` API endpoint. (#108835) --- .../common/elasticsearch_connection_status.ts | 5 - .../interactive_setup/server/config.test.ts | 43 ++ .../interactive_setup/server/config.ts | 10 + .../server/elasticsearch_service.mock.ts | 20 + .../server/elasticsearch_service.test.ts | 497 ++++++++++++++++++ .../server/elasticsearch_service.ts | 239 +++++++++ .../interactive_setup/server/errors.test.ts | 57 ++ .../interactive_setup/server/errors.ts | 35 ++ .../server/kibana_config_writer.mock.ts | 18 + .../server/kibana_config_writer.test.ts | 140 +++++ .../server/kibana_config_writer.ts | 93 ++++ src/plugins/interactive_setup/server/mocks.ts | 26 + .../interactive_setup/server/plugin.ts | 84 +-- .../server/routes/enroll.test.ts | 305 +++++++++++ .../interactive_setup/server/routes/enroll.ts | 89 +++- .../server/routes/index.mock.ts | 26 + .../interactive_setup/server/routes/index.ts | 12 +- 17 files changed, 1650 insertions(+), 49 deletions(-) create mode 100644 src/plugins/interactive_setup/server/config.test.ts create mode 100644 src/plugins/interactive_setup/server/elasticsearch_service.mock.ts create mode 100644 src/plugins/interactive_setup/server/elasticsearch_service.test.ts create mode 100644 src/plugins/interactive_setup/server/elasticsearch_service.ts create mode 100644 src/plugins/interactive_setup/server/errors.test.ts create mode 100644 src/plugins/interactive_setup/server/errors.ts create mode 100644 src/plugins/interactive_setup/server/kibana_config_writer.mock.ts create mode 100644 src/plugins/interactive_setup/server/kibana_config_writer.test.ts create mode 100644 src/plugins/interactive_setup/server/kibana_config_writer.ts create mode 100644 src/plugins/interactive_setup/server/mocks.ts create mode 100644 src/plugins/interactive_setup/server/routes/enroll.test.ts create mode 100644 src/plugins/interactive_setup/server/routes/index.mock.ts diff --git a/src/plugins/interactive_setup/common/elasticsearch_connection_status.ts b/src/plugins/interactive_setup/common/elasticsearch_connection_status.ts index 4e1506f69990c..bc0b172dfe234 100644 --- a/src/plugins/interactive_setup/common/elasticsearch_connection_status.ts +++ b/src/plugins/interactive_setup/common/elasticsearch_connection_status.ts @@ -10,11 +10,6 @@ * Describes current status of the Elasticsearch connection. */ export enum ElasticsearchConnectionStatus { - /** - * Indicates that Kibana hasn't figured out yet if existing Elasticsearch connection configuration is valid. - */ - Unknown = 'unknown', - /** * Indicates that current Elasticsearch connection configuration valid and sufficient. */ diff --git a/src/plugins/interactive_setup/server/config.test.ts b/src/plugins/interactive_setup/server/config.test.ts new file mode 100644 index 0000000000000..b8ae673ad28f9 --- /dev/null +++ b/src/plugins/interactive_setup/server/config.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ConfigSchema } from './config'; + +describe('config schema', () => { + it('generates proper defaults', () => { + expect(ConfigSchema.validate({})).toMatchInlineSnapshot(` + Object { + "connectionCheck": Object { + "interval": "PT5S", + }, + "enabled": false, + } + `); + }); + + describe('#connectionCheck', () => { + it('should properly set required connection check interval', () => { + expect(ConfigSchema.validate({ connectionCheck: { interval: '1s' } })).toMatchInlineSnapshot(` + Object { + "connectionCheck": Object { + "interval": "PT1S", + }, + "enabled": false, + } + `); + }); + + it('should throw error if interactiveSetup.connectionCheck.interval is less than 1 second', () => { + expect(() => + ConfigSchema.validate({ connectionCheck: { interval: 100 } }) + ).toThrowErrorMatchingInlineSnapshot( + `"[connectionCheck.interval]: the value must be greater or equal to 1 second."` + ); + }); + }); +}); diff --git a/src/plugins/interactive_setup/server/config.ts b/src/plugins/interactive_setup/server/config.ts index b16c51bcbda09..9986f16e9ce93 100644 --- a/src/plugins/interactive_setup/server/config.ts +++ b/src/plugins/interactive_setup/server/config.ts @@ -13,4 +13,14 @@ export type ConfigType = TypeOf; export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: false }), + connectionCheck: schema.object({ + interval: schema.duration({ + defaultValue: '5s', + validate(value) { + if (value.asSeconds() < 1) { + return 'the value must be greater or equal to 1 second.'; + } + }, + }), + }), }); diff --git a/src/plugins/interactive_setup/server/elasticsearch_service.mock.ts b/src/plugins/interactive_setup/server/elasticsearch_service.mock.ts new file mode 100644 index 0000000000000..8bc7e4307e76f --- /dev/null +++ b/src/plugins/interactive_setup/server/elasticsearch_service.mock.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { BehaviorSubject } from 'rxjs'; + +import { ElasticsearchConnectionStatus } from '../common'; + +export const elasticsearchServiceMock = { + createSetup: () => ({ + connectionStatus$: new BehaviorSubject( + ElasticsearchConnectionStatus.Configured + ), + enroll: jest.fn(), + }), +}; diff --git a/src/plugins/interactive_setup/server/elasticsearch_service.test.ts b/src/plugins/interactive_setup/server/elasticsearch_service.test.ts new file mode 100644 index 0000000000000..b8eb7293fd678 --- /dev/null +++ b/src/plugins/interactive_setup/server/elasticsearch_service.test.ts @@ -0,0 +1,497 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { errors } from '@elastic/elasticsearch'; + +import { nextTick } from '@kbn/test/jest'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; + +import { ElasticsearchConnectionStatus } from '../common'; +import { ConfigSchema } from './config'; +import type { ElasticsearchServiceSetup } from './elasticsearch_service'; +import { ElasticsearchService } from './elasticsearch_service'; +import { interactiveSetupMock } from './mocks'; + +describe('ElasticsearchService', () => { + let service: ElasticsearchService; + let mockElasticsearchPreboot: ReturnType; + beforeEach(() => { + service = new ElasticsearchService(loggingSystemMock.createLogger()); + mockElasticsearchPreboot = elasticsearchServiceMock.createPreboot(); + }); + + describe('#setup()', () => { + let mockConnectionStatusClient: ReturnType< + typeof elasticsearchServiceMock.createCustomClusterClient + >; + let mockEnrollClient: ReturnType; + let mockAuthenticateClient: ReturnType< + typeof elasticsearchServiceMock.createCustomClusterClient + >; + let setupContract: ElasticsearchServiceSetup; + beforeEach(() => { + mockConnectionStatusClient = elasticsearchServiceMock.createCustomClusterClient(); + mockEnrollClient = elasticsearchServiceMock.createCustomClusterClient(); + mockAuthenticateClient = elasticsearchServiceMock.createCustomClusterClient(); + mockElasticsearchPreboot.createClient.mockImplementation((type) => { + switch (type) { + case 'enroll': + return mockEnrollClient; + case 'authenticate': + return mockAuthenticateClient; + default: + return mockConnectionStatusClient; + } + }); + + setupContract = service.setup({ + elasticsearch: mockElasticsearchPreboot, + connectionCheckInterval: ConfigSchema.validate({}).connectionCheck.interval, + }); + }); + + describe('#connectionStatus$', () => { + beforeEach(() => jest.useFakeTimers()); + afterEach(() => jest.useRealTimers()); + + it('does not repeat ping request if have multiple subscriptions', async () => { + mockConnectionStatusClient.asInternalUser.ping.mockRejectedValue( + new errors.ConnectionError( + 'some-message', + interactiveSetupMock.createApiResponse({ body: {} }) + ) + ); + + const mockHandler1 = jest.fn(); + const mockHandler2 = jest.fn(); + setupContract.connectionStatus$.subscribe(mockHandler1); + setupContract.connectionStatus$.subscribe(mockHandler2); + + jest.advanceTimersByTime(0); + await nextTick(); + + // Late subscription. + const mockHandler3 = jest.fn(); + setupContract.connectionStatus$.subscribe(mockHandler3); + + jest.advanceTimersByTime(100); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).toHaveBeenCalledTimes(1); + expect(mockHandler1).toHaveBeenCalledTimes(1); + expect(mockHandler1).toHaveBeenCalledWith(ElasticsearchConnectionStatus.NotConfigured); + expect(mockHandler2).toHaveBeenCalledTimes(1); + expect(mockHandler2).toHaveBeenCalledWith(ElasticsearchConnectionStatus.NotConfigured); + expect(mockHandler3).toHaveBeenCalledTimes(1); + expect(mockHandler3).toHaveBeenCalledWith(ElasticsearchConnectionStatus.NotConfigured); + }); + + it('does not report the same status twice', async () => { + mockConnectionStatusClient.asInternalUser.ping.mockRejectedValue( + new errors.ConnectionError( + 'some-message', + interactiveSetupMock.createApiResponse({ body: {} }) + ) + ); + + const mockHandler = jest.fn(); + setupContract.connectionStatus$.subscribe(mockHandler); + + jest.advanceTimersByTime(0); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledWith(ElasticsearchConnectionStatus.NotConfigured); + + mockHandler.mockClear(); + + jest.advanceTimersByTime(5000); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).toHaveBeenCalledTimes(2); + expect(mockHandler).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(5000); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).toHaveBeenCalledTimes(3); + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('stops status checks as soon as connection is known to be configured', async () => { + mockConnectionStatusClient.asInternalUser.ping.mockRejectedValue( + new errors.ConnectionError( + 'some-message', + interactiveSetupMock.createApiResponse({ body: {} }) + ) + ); + + const mockHandler = jest.fn(); + setupContract.connectionStatus$.subscribe(mockHandler); + + jest.advanceTimersByTime(0); + await nextTick(); + + // Initial ping (connection error). + expect(mockConnectionStatusClient.asInternalUser.ping).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledWith(ElasticsearchConnectionStatus.NotConfigured); + + // Repeated ping (Unauthorized error). + mockConnectionStatusClient.asInternalUser.ping.mockRejectedValue( + new errors.ResponseError( + interactiveSetupMock.createApiResponse({ statusCode: 401, body: {} }) + ) + ); + jest.advanceTimersByTime(5000); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).toHaveBeenCalledTimes(2); + expect(mockHandler).toHaveBeenCalledTimes(2); + expect(mockHandler).toHaveBeenCalledWith(ElasticsearchConnectionStatus.Configured); + + mockHandler.mockClear(); + mockConnectionStatusClient.asInternalUser.ping.mockClear(); + + jest.advanceTimersByTime(5000); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).not.toHaveBeenCalled(); + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('checks connection status only once if connection is known to be configured right from start', async () => { + mockConnectionStatusClient.asInternalUser.ping.mockResolvedValue( + interactiveSetupMock.createApiResponse({ body: true }) + ); + + const mockHandler = jest.fn(); + setupContract.connectionStatus$.subscribe(mockHandler); + + jest.advanceTimersByTime(0); + await nextTick(); + + // Initial ping (connection error). + expect(mockConnectionStatusClient.asInternalUser.ping).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledWith(ElasticsearchConnectionStatus.Configured); + + mockHandler.mockClear(); + mockConnectionStatusClient.asInternalUser.ping.mockClear(); + + jest.advanceTimersByTime(5000); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).not.toHaveBeenCalled(); + expect(mockHandler).not.toHaveBeenCalled(); + + const mockHandler2 = jest.fn(); + setupContract.connectionStatus$.subscribe(mockHandler2); + + // Source observable is complete, and handler should be called immediately. + expect(mockHandler2).toHaveBeenCalledTimes(1); + expect(mockHandler2).toHaveBeenCalledWith(ElasticsearchConnectionStatus.Configured); + + mockHandler2.mockClear(); + + // No status check should be made after the first attempt. + jest.advanceTimersByTime(5000); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).not.toHaveBeenCalled(); + expect(mockHandler).not.toHaveBeenCalled(); + expect(mockHandler2).not.toHaveBeenCalled(); + }); + + it('does not check connection status if there are no subscribers', async () => { + mockConnectionStatusClient.asInternalUser.ping.mockRejectedValue( + new errors.ConnectionError( + 'some-message', + interactiveSetupMock.createApiResponse({ body: {} }) + ) + ); + + const mockHandler = jest.fn(); + const mockSubscription = setupContract.connectionStatus$.subscribe(mockHandler); + + jest.advanceTimersByTime(0); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledWith(ElasticsearchConnectionStatus.NotConfigured); + + mockSubscription.unsubscribe(); + mockHandler.mockClear(); + mockConnectionStatusClient.asInternalUser.ping.mockClear(); + + jest.advanceTimersByTime(5000); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).not.toHaveBeenCalled(); + expect(mockHandler).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(5000); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).not.toHaveBeenCalled(); + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('treats non-connection errors the same as successful response', async () => { + mockConnectionStatusClient.asInternalUser.ping.mockRejectedValue( + new errors.ResponseError( + interactiveSetupMock.createApiResponse({ statusCode: 401, body: {} }) + ) + ); + + const mockHandler = jest.fn(); + setupContract.connectionStatus$.subscribe(mockHandler); + + jest.advanceTimersByTime(0); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledWith(ElasticsearchConnectionStatus.Configured); + + mockHandler.mockClear(); + mockConnectionStatusClient.asInternalUser.ping.mockClear(); + + jest.advanceTimersByTime(5000); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).not.toHaveBeenCalled(); + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('treats product check error the same as successful response', async () => { + mockConnectionStatusClient.asInternalUser.ping.mockRejectedValue( + new errors.ProductNotSupportedError(interactiveSetupMock.createApiResponse({ body: {} })) + ); + + const mockHandler = jest.fn(); + setupContract.connectionStatus$.subscribe(mockHandler); + + jest.advanceTimersByTime(0); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledWith(ElasticsearchConnectionStatus.Configured); + + mockHandler.mockClear(); + mockConnectionStatusClient.asInternalUser.ping.mockClear(); + + jest.advanceTimersByTime(5000); + await nextTick(); + + expect(mockConnectionStatusClient.asInternalUser.ping).not.toHaveBeenCalled(); + expect(mockHandler).not.toHaveBeenCalled(); + }); + }); + + describe('#enroll()', () => { + it('fails if enroll call fails', async () => { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.transport.request.mockRejectedValue( + new errors.ResponseError( + interactiveSetupMock.createApiResponse({ statusCode: 401, body: { message: 'oh no' } }) + ) + ); + mockEnrollClient.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect( + setupContract.enroll({ apiKey: 'apiKey', hosts: ['host1'] }) + ).rejects.toMatchInlineSnapshot(`[ResponseError: {"message":"oh no"}]`); + + expect(mockEnrollClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockEnrollClient.close).toHaveBeenCalledTimes(1); + expect(mockAuthenticateClient.asInternalUser.security.authenticate).not.toHaveBeenCalled(); + }); + + it('fails if none of the hosts are accessible', async () => { + const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockScopedClusterClient.asCurrentUser.transport.request.mockRejectedValue( + new errors.ConnectionError( + 'some-message', + interactiveSetupMock.createApiResponse({ body: {} }) + ) + ); + mockEnrollClient.asScoped.mockReturnValue(mockScopedClusterClient); + + await expect( + setupContract.enroll({ apiKey: 'apiKey', hosts: ['host1', 'host2'] }) + ).rejects.toMatchInlineSnapshot(`[Error: Unable to connect to any of the provided hosts.]`); + + expect(mockEnrollClient.close).toHaveBeenCalledTimes(2); + expect(mockAuthenticateClient.asInternalUser.security.authenticate).not.toHaveBeenCalled(); + }); + + it('fails if authenticate call fails', async () => { + const mockEnrollScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockEnrollScopedClusterClient.asCurrentUser.transport.request.mockResolvedValue( + interactiveSetupMock.createApiResponse({ + statusCode: 200, + body: { token: { name: 'some-name', value: 'some-value' }, http_ca: 'some-ca' }, + }) + ); + mockEnrollClient.asScoped.mockReturnValue(mockEnrollScopedClusterClient); + + mockAuthenticateClient.asInternalUser.security.authenticate.mockRejectedValue( + new errors.ResponseError( + interactiveSetupMock.createApiResponse({ statusCode: 401, body: { message: 'oh no' } }) + ) + ); + + await expect( + setupContract.enroll({ apiKey: 'apiKey', hosts: ['host1'] }) + ).rejects.toMatchInlineSnapshot(`[ResponseError: {"message":"oh no"}]`); + + expect(mockEnrollClient.asScoped).toHaveBeenCalledTimes(1); + expect(mockEnrollClient.close).toHaveBeenCalledTimes(1); + expect(mockAuthenticateClient.asInternalUser.security.authenticate).toHaveBeenCalledTimes( + 1 + ); + expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1); + }); + + it('iterates through all provided hosts until find an accessible one', async () => { + mockElasticsearchPreboot.createClient.mockClear(); + + const mockHostOneEnrollScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockHostOneEnrollScopedClusterClient.asCurrentUser.transport.request.mockRejectedValue( + new errors.ConnectionError( + 'some-message', + interactiveSetupMock.createApiResponse({ body: {} }) + ) + ); + + const mockHostTwoEnrollScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient(); + mockHostTwoEnrollScopedClusterClient.asCurrentUser.transport.request.mockResolvedValue( + interactiveSetupMock.createApiResponse({ + statusCode: 200, + body: { + token: { name: 'some-name', value: 'some-value' }, + http_ca: '\n\nsome weird-ca_with\n content\n\n', + }, + }) + ); + + mockEnrollClient.asScoped + .mockReturnValueOnce(mockHostOneEnrollScopedClusterClient) + .mockReturnValueOnce(mockHostTwoEnrollScopedClusterClient); + + mockAuthenticateClient.asInternalUser.security.authenticate.mockResolvedValue( + interactiveSetupMock.createApiResponse({ statusCode: 200, body: {} as any }) + ); + + const expectedCa = `-----BEGIN CERTIFICATE----- + + +some weird+ca/with + + content + + +-----END CERTIFICATE----- +`; + + await expect( + setupContract.enroll({ apiKey: 'apiKey', hosts: ['host1', 'host2'] }) + ).resolves.toEqual({ + ca: expectedCa, + host: 'host2', + serviceAccountToken: { + name: 'some-name', + value: 'some-value', + }, + }); + + // Check that we created clients with the right parameters + expect(mockElasticsearchPreboot.createClient).toHaveBeenCalledTimes(3); + expect(mockElasticsearchPreboot.createClient).toHaveBeenCalledWith('enroll', { + hosts: ['host1'], + ssl: { verificationMode: 'none' }, + }); + expect(mockElasticsearchPreboot.createClient).toHaveBeenCalledWith('enroll', { + hosts: ['host2'], + ssl: { verificationMode: 'none' }, + }); + expect(mockElasticsearchPreboot.createClient).toHaveBeenCalledWith('authenticate', { + hosts: ['host2'], + serviceAccountToken: 'some-value', + ssl: { certificateAuthorities: [expectedCa] }, + }); + + // Check that we properly provided apiKeys to scoped clients. + expect(mockEnrollClient.asScoped).toHaveBeenCalledTimes(2); + expect(mockEnrollClient.asScoped).toHaveBeenNthCalledWith(1, { + headers: { authorization: 'ApiKey apiKey' }, + }); + expect(mockEnrollClient.asScoped).toHaveBeenNthCalledWith(2, { + headers: { authorization: 'ApiKey apiKey' }, + }); + + // Check that we properly called all required ES APIs. + expect( + mockHostOneEnrollScopedClusterClient.asCurrentUser.transport.request + ).toHaveBeenCalledTimes(1); + expect( + mockHostOneEnrollScopedClusterClient.asCurrentUser.transport.request + ).toHaveBeenCalledWith({ + method: 'GET', + path: '/_security/enroll/kibana', + }); + expect( + mockHostTwoEnrollScopedClusterClient.asCurrentUser.transport.request + ).toHaveBeenCalledTimes(1); + expect( + mockHostTwoEnrollScopedClusterClient.asCurrentUser.transport.request + ).toHaveBeenCalledWith({ + method: 'GET', + path: '/_security/enroll/kibana', + }); + expect(mockAuthenticateClient.asInternalUser.security.authenticate).toHaveBeenCalledTimes( + 1 + ); + + // Check that we properly closed all clients. + expect(mockEnrollClient.close).toHaveBeenCalledTimes(2); + expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1); + }); + }); + }); + + describe('#stop()', () => { + it('does not fail if called before `setup`', () => { + expect(() => service.stop()).not.toThrow(); + }); + + it('closes connection status check client', async () => { + const mockConnectionStatusClient = elasticsearchServiceMock.createCustomClusterClient(); + mockElasticsearchPreboot.createClient.mockImplementation((type) => { + switch (type) { + case 'ping': + return mockConnectionStatusClient; + default: + throw new Error(`Unexpected client type: ${type}`); + } + }); + + service.setup({ + elasticsearch: mockElasticsearchPreboot, + connectionCheckInterval: ConfigSchema.validate({}).connectionCheck.interval, + }); + service.stop(); + + expect(mockConnectionStatusClient.close).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/plugins/interactive_setup/server/elasticsearch_service.ts b/src/plugins/interactive_setup/server/elasticsearch_service.ts new file mode 100644 index 0000000000000..cad34e1a4d44a --- /dev/null +++ b/src/plugins/interactive_setup/server/elasticsearch_service.ts @@ -0,0 +1,239 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ApiResponse } from '@elastic/elasticsearch'; +import { errors } from '@elastic/elasticsearch'; +import type { Duration } from 'moment'; +import type { Observable } from 'rxjs'; +import { from, of, timer } from 'rxjs'; +import { + catchError, + distinctUntilChanged, + exhaustMap, + map, + shareReplay, + takeWhile, +} from 'rxjs/operators'; + +import type { + ElasticsearchClientConfig, + ElasticsearchServicePreboot, + ICustomClusterClient, + Logger, + ScopeableRequest, +} from 'src/core/server'; + +import { ElasticsearchConnectionStatus } from '../common'; +import { getDetailedErrorMessage } from './errors'; + +interface EnrollParameters { + apiKey: string; + hosts: string[]; + // TODO: Integrate fingerprint check as soon core supports this new option: + // https://github.com/elastic/kibana/pull/108514 + caFingerprint?: string; +} + +export interface ElasticsearchServiceSetupDeps { + /** + * Core Elasticsearch service preboot contract; + */ + elasticsearch: ElasticsearchServicePreboot; + + /** + * Interval for the Elasticsearch connection check (whether it's configured or not). + */ + connectionCheckInterval: Duration; +} + +export interface ElasticsearchServiceSetup { + /** + * Observable that yields the last result of the Elasticsearch connection status check. + */ + connectionStatus$: Observable; + + /** + * Iterates through provided {@param hosts} one by one trying to call Kibana enrollment API using + * the specified {@param apiKey}. + * @param apiKey The ApiKey to use to authenticate Kibana enrollment request. + * @param hosts The list of Elasticsearch node addresses to enroll with. The addresses are supposed + * to point to exactly same Elasticsearch node, potentially available via different network interfaces. + */ + enroll: (params: EnrollParameters) => Promise; +} + +/** + * Result of the enrollment request. + */ +export interface EnrollResult { + /** + * Host address of the Elasticsearch node that successfully processed enrollment request. + */ + host: string; + /** + * PEM CA certificate for the Elasticsearch HTTP certificates. + */ + ca: string; + /** + * Service account token for the "elastic/kibana" service account. + */ + serviceAccountToken: { name: string; value: string }; +} + +export class ElasticsearchService { + /** + * Elasticsearch client used to check Elasticsearch connection status. + */ + private connectionStatusClient?: ICustomClusterClient; + constructor(private readonly logger: Logger) {} + + public setup({ + elasticsearch, + connectionCheckInterval, + }: ElasticsearchServiceSetupDeps): ElasticsearchServiceSetup { + const connectionStatusClient = (this.connectionStatusClient = elasticsearch.createClient( + 'ping' + )); + + return { + connectionStatus$: timer(0, connectionCheckInterval.asMilliseconds()).pipe( + exhaustMap(() => { + return from(connectionStatusClient.asInternalUser.ping()).pipe( + map(() => ElasticsearchConnectionStatus.Configured), + catchError((pingError) => + of( + pingError instanceof errors.ConnectionError + ? ElasticsearchConnectionStatus.NotConfigured + : ElasticsearchConnectionStatus.Configured + ) + ) + ); + }), + takeWhile( + (status) => status !== ElasticsearchConnectionStatus.Configured, + /* inclusive */ true + ), + distinctUntilChanged(), + shareReplay({ refCount: true, bufferSize: 1 }) + ), + enroll: this.enroll.bind(this, elasticsearch), + }; + } + + public stop() { + if (this.connectionStatusClient) { + this.connectionStatusClient.close().catch((err) => { + this.logger.debug(`Failed to stop Elasticsearch service: ${getDetailedErrorMessage(err)}`); + }); + this.connectionStatusClient = undefined; + } + } + + /** + * Iterates through provided {@param hosts} one by one trying to call Kibana enrollment API using + * the specified {@param apiKey}. + * @param elasticsearch Core Elasticsearch service preboot contract. + * @param apiKey The ApiKey to use to authenticate Kibana enrollment request. + * @param hosts The list of Elasticsearch node addresses to enroll with. The addresses are supposed + * to point to exactly same Elasticsearch node, potentially available via different network interfaces. + */ + private async enroll( + elasticsearch: ElasticsearchServicePreboot, + { apiKey, hosts }: EnrollParameters + ): Promise { + const scopeableRequest: ScopeableRequest = { headers: { authorization: `ApiKey ${apiKey}` } }; + const elasticsearchConfig: Partial = { + ssl: { verificationMode: 'none' }, + }; + + // We should iterate through all provided hosts until we find an accessible one. + for (const host of hosts) { + this.logger.debug(`Trying to enroll with "${host}" host`); + const enrollClient = elasticsearch.createClient('enroll', { + ...elasticsearchConfig, + hosts: [host], + }); + + let enrollmentResponse; + try { + enrollmentResponse = (await enrollClient + .asScoped(scopeableRequest) + .asCurrentUser.transport.request({ + method: 'GET', + path: '/_security/enroll/kibana', + })) as ApiResponse<{ token: { name: string; value: string }; http_ca: string }>; + } catch (err) { + // We expect that all hosts belong to exactly same node and any non-connection error for one host would mean + // that enrollment will fail for any other host and we should bail out. + if (err instanceof errors.ConnectionError || err instanceof errors.TimeoutError) { + this.logger.error( + `Unable to connect to "${host}" host, will proceed to the next host if available: ${getDetailedErrorMessage( + err + )}` + ); + continue; + } + + this.logger.error(`Failed to enroll with "${host}" host: ${getDetailedErrorMessage(err)}`); + throw err; + } finally { + await enrollClient.close(); + } + + this.logger.debug( + `Successfully enrolled with "${host}" host, token name: ${enrollmentResponse.body.token.name}, CA certificate: ${enrollmentResponse.body.http_ca}` + ); + + const enrollResult = { + host, + ca: ElasticsearchService.createPemCertificate(enrollmentResponse.body.http_ca), + serviceAccountToken: enrollmentResponse.body.token, + }; + + // Now try to use retrieved password and CA certificate to authenticate to this host. + const authenticateClient = elasticsearch.createClient('authenticate', { + hosts: [host], + serviceAccountToken: enrollResult.serviceAccountToken.value, + ssl: { certificateAuthorities: [enrollResult.ca] }, + }); + + this.logger.debug( + `Verifying if "${enrollmentResponse.body.token.name}" token can authenticate to "${host}" host.` + ); + + try { + await authenticateClient.asInternalUser.security.authenticate(); + this.logger.debug( + `Successfully authenticated "${enrollmentResponse.body.token.name}" token to "${host}" host.` + ); + } catch (err) { + this.logger.error( + `Failed to authenticate "${ + enrollmentResponse.body.token.name + }" token to "${host}" host: ${getDetailedErrorMessage(err)}.` + ); + throw err; + } finally { + await authenticateClient.close(); + } + + return enrollResult; + } + + throw new Error('Unable to connect to any of the provided hosts.'); + } + + private static createPemCertificate(derCaString: string) { + // Use `X509Certificate` class once we upgrade to Node v16. + return `-----BEGIN CERTIFICATE-----\n${derCaString + .replace(/_/g, '/') + .replace(/-/g, '+') + .replace(/([^\n]{1,65})/g, '$1\n') + .replace(/\n$/g, '')}\n-----END CERTIFICATE-----\n`; + } +} diff --git a/src/plugins/interactive_setup/server/errors.test.ts b/src/plugins/interactive_setup/server/errors.test.ts new file mode 100644 index 0000000000000..e9ef64fb0d3d7 --- /dev/null +++ b/src/plugins/interactive_setup/server/errors.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { errors as esErrors } from '@elastic/elasticsearch'; + +import * as errors from './errors'; +import { interactiveSetupMock } from './mocks'; + +describe('errors', () => { + describe('#getErrorStatusCode', () => { + it('extracts status code from Elasticsearch client response error', () => { + expect( + errors.getErrorStatusCode( + new esErrors.ResponseError( + interactiveSetupMock.createApiResponse({ statusCode: 400, body: {} }) + ) + ) + ).toBe(400); + expect( + errors.getErrorStatusCode( + new esErrors.ResponseError( + interactiveSetupMock.createApiResponse({ statusCode: 401, body: {} }) + ) + ) + ).toBe(401); + }); + + it('extracts status code from `status` property', () => { + expect(errors.getErrorStatusCode({ statusText: 'Bad Request', status: 400 })).toBe(400); + expect(errors.getErrorStatusCode({ statusText: 'Unauthorized', status: 401 })).toBe(401); + }); + }); + + describe('#getDetailedErrorMessage', () => { + it('extracts body from Elasticsearch client response error', () => { + expect( + errors.getDetailedErrorMessage( + new esErrors.ResponseError( + interactiveSetupMock.createApiResponse({ + statusCode: 401, + body: { field1: 'value-1', field2: 'value-2' }, + }) + ) + ) + ).toBe(JSON.stringify({ field1: 'value-1', field2: 'value-2' })); + }); + + it('extracts `message` property', () => { + expect(errors.getDetailedErrorMessage(new Error('some-message'))).toBe('some-message'); + }); + }); +}); diff --git a/src/plugins/interactive_setup/server/errors.ts b/src/plugins/interactive_setup/server/errors.ts new file mode 100644 index 0000000000000..5f1d2388b3938 --- /dev/null +++ b/src/plugins/interactive_setup/server/errors.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { errors } from '@elastic/elasticsearch'; + +/** + * Extracts error code from Boom and Elasticsearch "native" errors. + * @param error Error instance to extract status code from. + */ +export function getErrorStatusCode(error: any): number { + if (error instanceof errors.ResponseError) { + return error.statusCode; + } + + return error.statusCode || error.status; +} + +/** + * Extracts detailed error message from Boom and Elasticsearch "native" errors. It's supposed to be + * only logged on the server side and never returned to the client as it may contain sensitive + * information. + * @param error Error instance to extract message from. + */ +export function getDetailedErrorMessage(error: any): string { + if (error instanceof errors.ResponseError) { + return JSON.stringify(error.body); + } + + return error.message; +} diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.mock.ts b/src/plugins/interactive_setup/server/kibana_config_writer.mock.ts new file mode 100644 index 0000000000000..d2c498e5fc077 --- /dev/null +++ b/src/plugins/interactive_setup/server/kibana_config_writer.mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; + +import type { KibanaConfigWriter } from './kibana_config_writer'; + +export const kibanaConfigWriterMock = { + create: (): jest.Mocked> => ({ + isConfigWritable: jest.fn().mockResolvedValue(true), + writeConfig: jest.fn().mockResolvedValue(undefined), + }), +}; diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts new file mode 100644 index 0000000000000..7ae98157ba156 --- /dev/null +++ b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +jest.mock('fs/promises'); +import { constants } from 'fs'; + +import { loggingSystemMock } from 'src/core/server/mocks'; + +import { KibanaConfigWriter } from './kibana_config_writer'; + +describe('KibanaConfigWriter', () => { + let mockFsAccess: jest.Mock; + let mockWriteFile: jest.Mock; + let mockAppendFile: jest.Mock; + let kibanaConfigWriter: KibanaConfigWriter; + beforeEach(() => { + jest.spyOn(Date, 'now').mockReturnValue(1234); + + const fsMocks = jest.requireMock('fs/promises'); + mockFsAccess = fsMocks.access; + mockWriteFile = fsMocks.writeFile; + mockAppendFile = fsMocks.appendFile; + + kibanaConfigWriter = new KibanaConfigWriter( + '/some/path/kibana.yml', + loggingSystemMock.createLogger() + ); + }); + + afterEach(() => jest.resetAllMocks()); + + describe('#isConfigWritable()', () => { + it('returns `false` if config directory is not writable even if kibana yml is writable', async () => { + mockFsAccess.mockImplementation((path, modifier) => + path === '/some/path' && modifier === constants.W_OK ? Promise.reject() : Promise.resolve() + ); + + await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(false); + }); + + it('returns `false` if kibana yml is NOT writable if even config directory is writable', async () => { + mockFsAccess.mockImplementation((path, modifier) => + path === '/some/path/kibana.yml' && modifier === constants.W_OK + ? Promise.reject() + : Promise.resolve() + ); + + await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(false); + }); + + it('returns `true` if both kibana yml and config directory are writable', async () => { + mockFsAccess.mockResolvedValue(undefined); + + await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(true); + }); + + it('returns `true` even if kibana yml does not exist when config directory is writable', async () => { + mockFsAccess.mockImplementation((path) => + path === '/some/path/kibana.yml' ? Promise.reject() : Promise.resolve() + ); + + await expect(kibanaConfigWriter.isConfigWritable()).resolves.toBe(true); + }); + }); + + describe('#writeConfig()', () => { + it('throws if cannot write CA file', async () => { + mockWriteFile.mockRejectedValue(new Error('Oh no!')); + + await expect( + kibanaConfigWriter.writeConfig({ + ca: 'ca-content', + host: '', + serviceAccountToken: { name: '', value: '' }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); + + expect(mockWriteFile).toHaveBeenCalledTimes(1); + expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); + expect(mockAppendFile).not.toHaveBeenCalled(); + }); + + it('throws if cannot append config to yaml file', async () => { + mockAppendFile.mockRejectedValue(new Error('Oh no!')); + + await expect( + kibanaConfigWriter.writeConfig({ + ca: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Oh no!]`); + + expect(mockWriteFile).toHaveBeenCalledTimes(1); + expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); + expect(mockAppendFile).toHaveBeenCalledTimes(1); + expect(mockAppendFile).toHaveBeenCalledWith( + '/some/path/kibana.yml', + ` + +# This section was automatically generated during setup (service account token name is "some-token"). +elasticsearch.hosts: [some-host] +elasticsearch.serviceAccountToken: some-value +elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] + +` + ); + }); + + it('can successfully write CA certificate and elasticsearch config to the disk', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + ca: 'ca-content', + host: 'some-host', + serviceAccountToken: { name: 'some-token', value: 'some-value' }, + }) + ).resolves.toBeUndefined(); + + expect(mockWriteFile).toHaveBeenCalledTimes(1); + expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); + expect(mockAppendFile).toHaveBeenCalledTimes(1); + expect(mockAppendFile).toHaveBeenCalledWith( + '/some/path/kibana.yml', + ` + +# This section was automatically generated during setup (service account token name is "some-token"). +elasticsearch.hosts: [some-host] +elasticsearch.serviceAccountToken: some-value +elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] + +` + ); + }); + }); +}); diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.ts b/src/plugins/interactive_setup/server/kibana_config_writer.ts new file mode 100644 index 0000000000000..b3178d9a909bd --- /dev/null +++ b/src/plugins/interactive_setup/server/kibana_config_writer.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { constants } from 'fs'; +import fs from 'fs/promises'; +import yaml from 'js-yaml'; +import path from 'path'; + +import type { Logger } from 'src/core/server'; + +import { getDetailedErrorMessage } from './errors'; + +export interface WriteConfigParameters { + host: string; + ca: string; + serviceAccountToken: { name: string; value: string }; +} + +export class KibanaConfigWriter { + constructor(private readonly configPath: string, private readonly logger: Logger) {} + + /** + * Checks if we can write to the Kibana configuration file and configuration directory. + */ + public async isConfigWritable() { + try { + // We perform two separate checks here: + // 1. If we can write to config directory to add a new CA certificate file and potentially Kibana configuration + // file if it doesn't exist for some reason. + // 2. If we can write to the Kibana configuration file if it exists. + const canWriteToConfigDirectory = fs.access(path.dirname(this.configPath), constants.W_OK); + await Promise.all([ + canWriteToConfigDirectory, + fs.access(this.configPath, constants.F_OK).then( + () => fs.access(this.configPath, constants.W_OK), + () => canWriteToConfigDirectory + ), + ]); + return true; + } catch { + return false; + } + } + + /** + * Writes Elasticsearch configuration to the disk. + * @param params + */ + public async writeConfig(params: WriteConfigParameters) { + const caPath = path.join(path.dirname(this.configPath), `ca_${Date.now()}.crt`); + + this.logger.debug(`Writing CA certificate to ${caPath}.`); + try { + await fs.writeFile(caPath, params.ca); + this.logger.debug(`Successfully wrote CA certificate to ${caPath}.`); + } catch (err) { + this.logger.error( + `Failed to write CA certificate to ${caPath}: ${getDetailedErrorMessage(err)}.` + ); + throw err; + } + + this.logger.debug(`Writing Elasticsearch configuration to ${this.configPath}.`); + try { + await fs.appendFile( + this.configPath, + `\n\n# This section was automatically generated during setup (service account token name is "${ + params.serviceAccountToken.name + }").\n${yaml.safeDump( + { + 'elasticsearch.hosts': [params.host], + 'elasticsearch.serviceAccountToken': params.serviceAccountToken.value, + 'elasticsearch.ssl.certificateAuthorities': [caPath], + }, + { flowLevel: 1 } + )}\n` + ); + this.logger.debug(`Successfully wrote Elasticsearch configuration to ${this.configPath}.`); + } catch (err) { + this.logger.error( + `Failed to write Elasticsearch configuration to ${ + this.configPath + }: ${getDetailedErrorMessage(err)}.` + ); + throw err; + } + } +} diff --git a/src/plugins/interactive_setup/server/mocks.ts b/src/plugins/interactive_setup/server/mocks.ts new file mode 100644 index 0000000000000..75b28a502b6d4 --- /dev/null +++ b/src/plugins/interactive_setup/server/mocks.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ApiResponse } from '@elastic/elasticsearch'; + +function createApiResponseMock( + apiResponse: Pick, 'body'> & + Partial, 'body'>> +): ApiResponse { + return { + statusCode: null, + headers: null, + warnings: null, + meta: {} as any, + ...apiResponse, + }; +} + +export const interactiveSetupMock = { + createApiResponse: createApiResponseMock, +}; diff --git a/src/plugins/interactive_setup/server/plugin.ts b/src/plugins/interactive_setup/server/plugin.ts index 6b2a12bad76bc..06ece32ba9c4e 100644 --- a/src/plugins/interactive_setup/server/plugin.ts +++ b/src/plugins/interactive_setup/server/plugin.ts @@ -13,11 +13,18 @@ import type { CorePreboot, Logger, PluginInitializerContext, PrebootPlugin } fro import { ElasticsearchConnectionStatus } from '../common'; import type { ConfigSchema, ConfigType } from './config'; +import { ElasticsearchService } from './elasticsearch_service'; +import { KibanaConfigWriter } from './kibana_config_writer'; import { defineRoutes } from './routes'; export class UserSetupPlugin implements PrebootPlugin { readonly #logger: Logger; + #elasticsearchConnectionStatusSubscription?: Subscription; + readonly #elasticsearch = new ElasticsearchService( + this.initializerContext.logger.get('elasticsearch') + ); + #configSubscription?: Subscription; #config?: ConfigType; readonly #getConfig = () => { @@ -27,11 +34,6 @@ export class UserSetupPlugin implements PrebootPlugin { return this.#config; }; - #elasticsearchConnectionStatus = ElasticsearchConnectionStatus.Unknown; - readonly #getElasticsearchConnectionStatus = () => { - return this.#elasticsearchConnectionStatus; - }; - constructor(private readonly initializerContext: PluginInitializerContext) { this.#logger = this.initializerContext.logger.get(); } @@ -65,45 +67,48 @@ export class UserSetupPlugin implements PrebootPlugin { }) ); - // If preliminary check above indicates that user didn't alter default Elasticsearch connection - // details, it doesn't mean Elasticsearch connection isn't configured. There is a chance that they - // already disabled security features in Elasticsearch and everything should work by default. - // We should check if we can connect to Elasticsearch with default configuration to know if we - // need to activate interactive setup. This check can take some time, so we should register our - // routes to let interactive setup UI to handle user requests until the check is complete. - core.elasticsearch - .createClient('ping') - .asInternalUser.ping() - .then( - (pingResponse) => { - if (pingResponse.body) { - this.#logger.debug( - 'Kibana is already properly configured to connect to Elasticsearch. Interactive setup mode will not be activated.' - ); - this.#elasticsearchConnectionStatus = ElasticsearchConnectionStatus.Configured; - completeSetup({ shouldReloadConfig: false }); - } else { - this.#logger.debug( - 'Kibana is not properly configured to connect to Elasticsearch. Interactive setup mode will be activated.' - ); - this.#elasticsearchConnectionStatus = ElasticsearchConnectionStatus.NotConfigured; - } - }, - () => { - // TODO: we should probably react differently to different errors. 401 - credentials aren't correct, etc. - // Do we want to constantly ping ES if interactive mode UI isn't active? Just in case user runs Kibana and then - // configure Elasticsearch so that it can eventually connect to it without any configuration changes? - this.#elasticsearchConnectionStatus = ElasticsearchConnectionStatus.NotConfigured; + // If preliminary checks above indicate that user didn't alter default Elasticsearch connection + // details, it doesn't mean Elasticsearch connection isn't configured. There is a chance that + // user has already disabled security features in Elasticsearch and everything should work by + // default. We should check if we can connect to Elasticsearch with default configuration to + // know if we need to activate interactive setup. This check can take some time, so we should + // register our routes to let interactive setup UI to handle user requests until the check is + // complete. Moreover Elasticsearch may be just temporarily unavailable and we should poll its + // status until we can connect or use configures connection via interactive setup mode. + const elasticsearch = this.#elasticsearch.setup({ + elasticsearch: core.elasticsearch, + connectionCheckInterval: this.#getConfig().connectionCheck.interval, + }); + this.#elasticsearchConnectionStatusSubscription = elasticsearch.connectionStatus$.subscribe( + (status) => { + if (status === ElasticsearchConnectionStatus.Configured) { + this.#logger.debug( + 'Skipping interactive setup mode since Kibana is already properly configured to connect to Elasticsearch at http://localhost:9200.' + ); + completeSetup({ shouldReloadConfig: false }); + } else { + this.#logger.debug( + 'Starting interactive setup mode since Kibana cannot to connect to Elasticsearch at http://localhost:9200.' + ); } - ); + } + ); + + // If possible, try to use `*.dev.yml` config when Kibana is run in development mode. + const configPath = this.initializerContext.env.mode.dev + ? this.initializerContext.env.configs.find((config) => config.endsWith('.dev.yml')) ?? + this.initializerContext.env.configs[0] + : this.initializerContext.env.configs[0]; core.http.registerRoutes('', (router) => { defineRoutes({ router, basePath: core.http.basePath, logger: this.#logger.get('routes'), + preboot: { ...core.preboot, completeSetup }, + kibanaConfigWriter: new KibanaConfigWriter(configPath, this.#logger.get('kibana-config')), + elasticsearch, getConfig: this.#getConfig.bind(this), - getElasticsearchConnectionStatus: this.#getElasticsearchConnectionStatus.bind(this), }); }); } @@ -115,5 +120,12 @@ export class UserSetupPlugin implements PrebootPlugin { this.#configSubscription.unsubscribe(); this.#configSubscription = undefined; } + + if (this.#elasticsearchConnectionStatusSubscription) { + this.#elasticsearchConnectionStatusSubscription.unsubscribe(); + this.#elasticsearchConnectionStatusSubscription = undefined; + } + + this.#elasticsearch.stop(); } } diff --git a/src/plugins/interactive_setup/server/routes/enroll.test.ts b/src/plugins/interactive_setup/server/routes/enroll.test.ts new file mode 100644 index 0000000000000..4fc91e5252480 --- /dev/null +++ b/src/plugins/interactive_setup/server/routes/enroll.test.ts @@ -0,0 +1,305 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { errors } from '@elastic/elasticsearch'; + +import type { ObjectType } from '@kbn/config-schema'; +import type { IRouter, RequestHandler, RequestHandlerContext, RouteConfig } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { ElasticsearchConnectionStatus } from '../../common'; +import { interactiveSetupMock } from '../mocks'; +import { defineEnrollRoutes } from './enroll'; +import { routeDefinitionParamsMock } from './index.mock'; + +describe('Enroll routes', () => { + let router: jest.Mocked; + let mockRouteParams: ReturnType; + let mockContext: RequestHandlerContext; + beforeEach(() => { + mockRouteParams = routeDefinitionParamsMock.create(); + router = mockRouteParams.router; + + mockContext = ({} as unknown) as RequestHandlerContext; + + defineEnrollRoutes(mockRouteParams); + }); + + describe('#enroll', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + beforeEach(() => { + const [enrollRouteConfig, enrollRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/internal/interactive_setup/enroll' + )!; + + routeConfig = enrollRouteConfig; + routeHandler = enrollRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toEqual({ authRequired: false }); + + const bodySchema = (routeConfig.validate as any).body as ObjectType; + expect(() => bodySchema.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[hosts]: expected value of type [array] but got [undefined]"` + ); + + expect(() => bodySchema.validate({ hosts: [] })).toThrowErrorMatchingInlineSnapshot( + `"[hosts]: array size is [0], but cannot be smaller than [1]"` + ); + expect(() => + bodySchema.validate({ hosts: ['localhost:9200'] }) + ).toThrowErrorMatchingInlineSnapshot(`"[hosts.0]: expected URI with scheme [https]."`); + expect(() => + bodySchema.validate({ hosts: ['http://localhost:9200'] }) + ).toThrowErrorMatchingInlineSnapshot(`"[hosts.0]: expected URI with scheme [https]."`); + expect(() => + bodySchema.validate({ + apiKey: 'some-key', + hosts: ['https://localhost:9200', 'http://localhost:9243'], + }) + ).toThrowErrorMatchingInlineSnapshot(`"[hosts.1]: expected URI with scheme [https]."`); + + expect(() => + bodySchema.validate({ hosts: ['https://localhost:9200'] }) + ).toThrowErrorMatchingInlineSnapshot( + `"[apiKey]: expected value of type [string] but got [undefined]"` + ); + expect(() => + bodySchema.validate({ apiKey: '', hosts: ['https://localhost:9200'] }) + ).toThrowErrorMatchingInlineSnapshot( + `"[apiKey]: value has length [0] but it must have a minimum length of [1]."` + ); + + expect(() => + bodySchema.validate({ apiKey: 'some-key', hosts: ['https://localhost:9200'] }) + ).toThrowErrorMatchingInlineSnapshot( + `"[caFingerprint]: expected value of type [string] but got [undefined]"` + ); + expect(() => + bodySchema.validate({ + apiKey: 'some-key', + hosts: ['https://localhost:9200'], + caFingerprint: '12345', + }) + ).toThrowErrorMatchingInlineSnapshot( + `"[caFingerprint]: value has length [5] but it must have a minimum length of [64]."` + ); + + expect( + bodySchema.validate( + bodySchema.validate({ + apiKey: 'some-key', + hosts: ['https://localhost:9200'], + caFingerprint: 'a'.repeat(64), + }) + ) + ).toEqual({ + apiKey: 'some-key', + hosts: ['https://localhost:9200'], + caFingerprint: 'a'.repeat(64), + }); + }); + + it('fails if setup is not on hold.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(false); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 400, + options: { body: 'Cannot process request outside of preboot stage.' }, + payload: 'Cannot process request outside of preboot stage.', + }); + + expect(mockRouteParams.elasticsearch.enroll).not.toHaveBeenCalled(); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('fails if Elasticsearch connection is already configured.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.connectionStatus$.next( + ElasticsearchConnectionStatus.Configured + ); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 400, + options: { + body: { + message: 'Elasticsearch connection is already configured.', + attributes: { type: 'elasticsearch_connection_configured' }, + }, + }, + payload: { + message: 'Elasticsearch connection is already configured.', + attributes: { type: 'elasticsearch_connection_configured' }, + }, + }); + + expect(mockRouteParams.elasticsearch.enroll).not.toHaveBeenCalled(); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('fails if Kibana config is not writable.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.connectionStatus$.next( + ElasticsearchConnectionStatus.NotConfigured + ); + mockRouteParams.kibanaConfigWriter.isConfigWritable.mockResolvedValue(false); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 500, + options: { + body: { + message: 'Kibana process does not have enough permissions to write to config file.', + attributes: { type: 'kibana_config_not_writable' }, + }, + statusCode: 500, + }, + payload: { + message: 'Kibana process does not have enough permissions to write to config file.', + attributes: { type: 'kibana_config_not_writable' }, + }, + }); + + expect(mockRouteParams.elasticsearch.enroll).not.toHaveBeenCalled(); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('fails if enroll call fails.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.connectionStatus$.next( + ElasticsearchConnectionStatus.NotConfigured + ); + mockRouteParams.kibanaConfigWriter.isConfigWritable.mockResolvedValue(true); + mockRouteParams.elasticsearch.enroll.mockRejectedValue( + new errors.ResponseError( + interactiveSetupMock.createApiResponse({ + statusCode: 401, + body: { message: 'some-secret-message' }, + }) + ) + ); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 500, + options: { + body: { message: 'Failed to enroll.', attributes: { type: 'enroll_failure' } }, + statusCode: 500, + }, + payload: { message: 'Failed to enroll.', attributes: { type: 'enroll_failure' } }, + }); + + expect(mockRouteParams.elasticsearch.enroll).toHaveBeenCalledTimes(1); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('fails if cannot write configuration to the disk.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.connectionStatus$.next( + ElasticsearchConnectionStatus.NotConfigured + ); + mockRouteParams.kibanaConfigWriter.isConfigWritable.mockResolvedValue(true); + mockRouteParams.elasticsearch.enroll.mockResolvedValue({ + ca: 'some-ca', + host: 'host', + serviceAccountToken: { name: 'some-name', value: 'some-value' }, + }); + mockRouteParams.kibanaConfigWriter.writeConfig.mockRejectedValue( + new Error('Some error with sensitive path') + ); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 500, + options: { + body: { + message: 'Failed to save configuration.', + attributes: { type: 'kibana_config_failure' }, + }, + statusCode: 500, + }, + payload: { + message: 'Failed to save configuration.', + attributes: { type: 'kibana_config_failure' }, + }, + }); + + expect(mockRouteParams.elasticsearch.enroll).toHaveBeenCalledTimes(1); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).toHaveBeenCalledTimes(1); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('can successfully enrol and save configuration to the disk.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.connectionStatus$.next( + ElasticsearchConnectionStatus.NotConfigured + ); + mockRouteParams.kibanaConfigWriter.isConfigWritable.mockResolvedValue(true); + mockRouteParams.elasticsearch.enroll.mockResolvedValue({ + ca: 'some-ca', + host: 'host', + serviceAccountToken: { name: 'some-name', value: 'some-value' }, + }); + mockRouteParams.kibanaConfigWriter.writeConfig.mockResolvedValue(); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { apiKey: 'some-key', hosts: ['host1', 'host2'], caFingerprint: 'ab:cd:ef' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 204, + options: {}, + payload: undefined, + }); + + expect(mockRouteParams.elasticsearch.enroll).toHaveBeenCalledTimes(1); + expect(mockRouteParams.elasticsearch.enroll).toHaveBeenCalledWith({ + apiKey: 'some-key', + hosts: ['host1', 'host2'], + caFingerprint: 'ab:cd:ef', + }); + + expect(mockRouteParams.kibanaConfigWriter.writeConfig).toHaveBeenCalledTimes(1); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).toHaveBeenCalledWith({ + ca: 'some-ca', + host: 'host', + serviceAccountToken: { name: 'some-name', value: 'some-value' }, + }); + + expect(mockRouteParams.preboot.completeSetup).toHaveBeenCalledTimes(1); + expect(mockRouteParams.preboot.completeSetup).toHaveBeenCalledWith({ + shouldReloadConfig: true, + }); + }); + }); +}); diff --git a/src/plugins/interactive_setup/server/routes/enroll.ts b/src/plugins/interactive_setup/server/routes/enroll.ts index a600d18109760..91b391bf8b109 100644 --- a/src/plugins/interactive_setup/server/routes/enroll.ts +++ b/src/plugins/interactive_setup/server/routes/enroll.ts @@ -6,26 +6,105 @@ * Side Public License, v 1. */ +import { first } from 'rxjs/operators'; + import { schema } from '@kbn/config-schema'; +import { ElasticsearchConnectionStatus } from '../../common'; +import type { EnrollResult } from '../elasticsearch_service'; import type { RouteDefinitionParams } from './'; /** * Defines routes to deal with Elasticsearch `enroll_kibana` APIs. */ -export function defineEnrollRoutes({ router }: RouteDefinitionParams) { +export function defineEnrollRoutes({ + router, + logger, + kibanaConfigWriter, + elasticsearch, + preboot, +}: RouteDefinitionParams) { router.post( { path: '/internal/interactive_setup/enroll', validate: { - body: schema.object({ token: schema.string() }), + body: schema.object({ + hosts: schema.arrayOf(schema.uri({ scheme: 'https' }), { + minSize: 1, + }), + apiKey: schema.string({ minLength: 1 }), + caFingerprint: schema.string({ maxLength: 64, minLength: 64 }), + }), }, options: { authRequired: false }, }, async (context, request, response) => { - return response.forbidden({ - body: { message: `API is not implemented yet.` }, - }); + if (!preboot.isSetupOnHold()) { + logger.error(`Invalid request to [path=${request.url.pathname}] outside of preboot stage`); + return response.badRequest({ body: 'Cannot process request outside of preboot stage.' }); + } + + const connectionStatus = await elasticsearch.connectionStatus$.pipe(first()).toPromise(); + if (connectionStatus === ElasticsearchConnectionStatus.Configured) { + logger.error( + `Invalid request to [path=${request.url.pathname}], Elasticsearch connection is already configured.` + ); + return response.badRequest({ + body: { + message: 'Elasticsearch connection is already configured.', + attributes: { type: 'elasticsearch_connection_configured' }, + }, + }); + } + + // The most probable misconfiguration case is when Kibana process isn't allowed to write to the + // Kibana configuration file. We'll still have to handle possible filesystem access errors + // when we actually write to the disk, but this preliminary check helps us to avoid unnecessary + // enrollment call and communicate that to the user early. + const isConfigWritable = await kibanaConfigWriter.isConfigWritable(); + if (!isConfigWritable) { + logger.error('Kibana process does not have enough permissions to write to config file'); + return response.customError({ + statusCode: 500, + body: { + message: 'Kibana process does not have enough permissions to write to config file.', + attributes: { type: 'kibana_config_not_writable' }, + }, + }); + } + + let enrollResult: EnrollResult; + try { + enrollResult = await elasticsearch.enroll({ + apiKey: request.body.apiKey, + hosts: request.body.hosts, + caFingerprint: request.body.caFingerprint, + }); + } catch { + // For security reasons, we shouldn't leak to the user whether Elasticsearch node couldn't process enrollment + // request or we just couldn't connect to any of the provided hosts. + return response.customError({ + statusCode: 500, + body: { message: 'Failed to enroll.', attributes: { type: 'enroll_failure' } }, + }); + } + + try { + await kibanaConfigWriter.writeConfig(enrollResult); + } catch { + // For security reasons, we shouldn't leak any filesystem related errors. + return response.customError({ + statusCode: 500, + body: { + message: 'Failed to save configuration.', + attributes: { type: 'kibana_config_failure' }, + }, + }); + } + + preboot.completeSetup({ shouldReloadConfig: true }); + + return response.noContent(); } ); } diff --git a/src/plugins/interactive_setup/server/routes/index.mock.ts b/src/plugins/interactive_setup/server/routes/index.mock.ts new file mode 100644 index 0000000000000..249d1277269e7 --- /dev/null +++ b/src/plugins/interactive_setup/server/routes/index.mock.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { coreMock, httpServiceMock, loggingSystemMock } from 'src/core/server/mocks'; + +import { ConfigSchema } from '../config'; +import { elasticsearchServiceMock } from '../elasticsearch_service.mock'; +import { kibanaConfigWriterMock } from '../kibana_config_writer.mock'; + +export const routeDefinitionParamsMock = { + create: (config: Record = {}) => ({ + router: httpServiceMock.createRouter(), + basePath: httpServiceMock.createBasePath(), + csp: httpServiceMock.createSetupContract().csp, + logger: loggingSystemMock.create().get(), + preboot: { ...coreMock.createPreboot().preboot, completeSetup: jest.fn() }, + getConfig: jest.fn().mockReturnValue(ConfigSchema.validate(config)), + elasticsearch: elasticsearchServiceMock.createSetup(), + kibanaConfigWriter: kibanaConfigWriterMock.create(), + }), +}; diff --git a/src/plugins/interactive_setup/server/routes/index.ts b/src/plugins/interactive_setup/server/routes/index.ts index 0f14f5ffac8ec..752c5828ecb59 100644 --- a/src/plugins/interactive_setup/server/routes/index.ts +++ b/src/plugins/interactive_setup/server/routes/index.ts @@ -6,10 +6,12 @@ * Side Public License, v 1. */ -import type { IBasePath, IRouter, Logger } from 'src/core/server'; +import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { IBasePath, IRouter, Logger, PrebootServicePreboot } from 'src/core/server'; -import type { ElasticsearchConnectionStatus } from '../../common'; import type { ConfigType } from '../config'; +import type { ElasticsearchServiceSetup } from '../elasticsearch_service'; +import type { KibanaConfigWriter } from '../kibana_config_writer'; import { defineEnrollRoutes } from './enroll'; /** @@ -19,8 +21,12 @@ export interface RouteDefinitionParams { readonly router: IRouter; readonly basePath: IBasePath; readonly logger: Logger; + readonly preboot: PrebootServicePreboot & { + completeSetup: (result: { shouldReloadConfig: boolean }) => void; + }; + readonly kibanaConfigWriter: PublicMethodsOf; + readonly elasticsearch: ElasticsearchServiceSetup; readonly getConfig: () => ConfigType; - readonly getElasticsearchConnectionStatus: () => ElasticsearchConnectionStatus; } export function defineRoutes(params: RouteDefinitionParams) { From 4419efcbd0b750cd8b5432229ad229f2c5a2ceb1 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Thu, 19 Aug 2021 12:36:23 +0300 Subject: [PATCH 23/28] [XY, Pie] Long legend values support (#108365) * [XY, Pie] Long legend values support * Update vislib snapshots * Fix truncate labels to work only for slice labels positioned outside the chart * Address PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/components/options/index.ts | 1 + .../options/long_legend_options.test.tsx | 48 +++++++++++++ .../options/long_legend_options.tsx | 71 +++++++++++++++++++ .../public/__snapshots__/pie_fn.test.ts.snap | 2 + .../public/editor/components/pie.test.tsx | 14 ++++ .../public/editor/components/pie.tsx | 15 +++- .../editor/components/truncate_labels.tsx | 12 +++- src/plugins/vis_type_pie/public/mocks.ts | 2 + .../vis_type_pie/public/pie_component.tsx | 11 ++- .../vis_type_pie/public/pie_fn.test.ts | 2 + src/plugins/vis_type_pie/public/pie_fn.ts | 13 ++++ .../public/sample_vis.test.mocks.ts | 2 + src/plugins/vis_type_pie/public/to_ast.ts | 2 + .../vis_type_pie/public/types/types.ts | 2 + .../public/utils/get_columns.test.ts | 2 + .../vis_type_pie/public/utils/get_config.ts | 1 + .../vis_type_pie/public/utils/get_layers.ts | 7 +- .../vis_type_pie/public/vis_type/pie.ts | 2 + .../public/__snapshots__/to_ast.test.ts.snap | 2 +- .../public/__snapshots__/to_ast.test.ts.snap | 6 ++ .../public/components/xy_settings.tsx | 7 ++ .../point_series/point_series.mocks.ts | 4 ++ .../point_series/point_series.test.tsx | 20 ++++++ .../options/point_series/point_series.tsx | 14 +++- .../public/expression_functions/xy_vis_fn.ts | 14 ++++ .../public/sample_vis.test.mocks.ts | 4 ++ src/plugins/vis_type_xy/public/to_ast.ts | 2 + src/plugins/vis_type_xy/public/types/param.ts | 4 ++ .../vis_type_xy/public/vis_component.tsx | 2 + .../vis_type_xy/public/vis_types/area.ts | 2 + .../vis_type_xy/public/vis_types/histogram.ts | 2 + .../public/vis_types/horizontal_bar.ts | 2 + .../vis_type_xy/public/vis_types/line.ts | 2 + 33 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 src/plugins/vis_default_editor/public/components/options/long_legend_options.test.tsx create mode 100644 src/plugins/vis_default_editor/public/components/options/long_legend_options.tsx diff --git a/src/plugins/vis_default_editor/public/components/options/index.ts b/src/plugins/vis_default_editor/public/components/options/index.ts index 31b09977f5c99..62ce76014f9fc 100644 --- a/src/plugins/vis_default_editor/public/components/options/index.ts +++ b/src/plugins/vis_default_editor/public/components/options/index.ts @@ -16,3 +16,4 @@ export { RangeOption } from './range'; export { RequiredNumberInputOption } from './required_number_input'; export { TextInputOption } from './text_input'; export { PercentageModeOption } from './percentage_mode'; +export { LongLegendOptions } from './long_legend_options'; diff --git a/src/plugins/vis_default_editor/public/components/options/long_legend_options.test.tsx b/src/plugins/vis_default_editor/public/components/options/long_legend_options.test.tsx new file mode 100644 index 0000000000000..69994bb279278 --- /dev/null +++ b/src/plugins/vis_default_editor/public/components/options/long_legend_options.test.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { LongLegendOptions, LongLegendOptionsProps } from './long_legend_options'; +import { EuiFieldNumber } from '@elastic/eui'; + +describe('LongLegendOptions', () => { + let props: LongLegendOptionsProps; + let component; + beforeAll(() => { + props = { + truncateLegend: true, + setValue: jest.fn(), + }; + }); + + it('renders the EuiFieldNumber', () => { + component = mountWithIntl(); + expect(component.find(EuiFieldNumber).length).toBe(1); + }); + + it('should call setValue when value is changes in the number input', () => { + component = mountWithIntl(); + const numberField = component.find(EuiFieldNumber); + numberField.props().onChange!(({ + target: { + value: 3, + }, + } as unknown) as React.ChangeEvent); + + expect(props.setValue).toHaveBeenCalledWith('maxLegendLines', 3); + }); + + it('input number should be disabled when truncate is false', () => { + props.truncateLegend = false; + component = mountWithIntl(); + const numberField = component.find(EuiFieldNumber); + + expect(numberField.props().disabled).toBeTruthy(); + }); +}); diff --git a/src/plugins/vis_default_editor/public/components/options/long_legend_options.tsx b/src/plugins/vis_default_editor/public/components/options/long_legend_options.tsx new file mode 100644 index 0000000000000..c06fb94376dbe --- /dev/null +++ b/src/plugins/vis_default_editor/public/components/options/long_legend_options.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; +import { SwitchOption } from './switch'; + +const MAX_TRUNCATE_LINES = 5; +const MIN_TRUNCATE_LINES = 1; + +export interface LongLegendOptionsProps { + setValue: (paramName: 'maxLegendLines' | 'truncateLegend', value: boolean | number) => void; + truncateLegend: boolean; + maxLegendLines?: number; + 'data-test-subj'?: string; +} + +function LongLegendOptions({ + 'data-test-subj': dataTestSubj, + setValue, + truncateLegend, + maxLegendLines, +}: LongLegendOptionsProps) { + return ( + <> + + + } + > + { + const val = Number(e.target.value); + setValue( + 'maxLegendLines', + Math.min(MAX_TRUNCATE_LINES, Math.max(val, MIN_TRUNCATE_LINES)) + ); + }} + /> + + + ); +} + +export { LongLegendOptions }; diff --git a/src/plugins/vis_type_pie/public/__snapshots__/pie_fn.test.ts.snap b/src/plugins/vis_type_pie/public/__snapshots__/pie_fn.test.ts.snap index 6c43072b97c28..fb51717d1adc0 100644 --- a/src/plugins/vis_type_pie/public/__snapshots__/pie_fn.test.ts.snap +++ b/src/plugins/vis_type_pie/public/__snapshots__/pie_fn.test.ts.snap @@ -57,6 +57,7 @@ Object { "valuesFormat": "percent", }, "legendPosition": "right", + "maxLegendLines": true, "metric": Object { "accessor": 0, "aggType": "count", @@ -72,6 +73,7 @@ Object { }, "splitColumn": undefined, "splitRow": undefined, + "truncateLegend": true, }, "visData": Object { "columns": Array [ diff --git a/src/plugins/vis_type_pie/public/editor/components/pie.test.tsx b/src/plugins/vis_type_pie/public/editor/components/pie.test.tsx index 524986524fd7e..d37f4c10ea9ea 100644 --- a/src/plugins/vis_type_pie/public/editor/components/pie.test.tsx +++ b/src/plugins/vis_type_pie/public/editor/components/pie.test.tsx @@ -73,6 +73,20 @@ describe('PalettePicker', function () { }); }); + it('renders the long legend options for the elastic charts implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'pieLongLegendsOptions').length).toBe(1); + }); + }); + + it('not renders the long legend options for the vislib implementation', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'pieLongLegendsOptions').length).toBe(0); + }); + }); + it('renders the label position dropdown for the elastic charts implementation', async () => { component = mountWithIntl(); await act(async () => { diff --git a/src/plugins/vis_type_pie/public/editor/components/pie.tsx b/src/plugins/vis_type_pie/public/editor/components/pie.tsx index 8ce4f4defbaed..3bf28ba58d4eb 100644 --- a/src/plugins/vis_type_pie/public/editor/components/pie.tsx +++ b/src/plugins/vis_type_pie/public/editor/components/pie.tsx @@ -26,6 +26,7 @@ import { SwitchOption, SelectOption, PalettePicker, + LongLegendOptions, } from '../../../../vis_default_editor/public'; import { VisEditorOptionsProps } from '../../../../visualizations/public'; import { TruncateLabelsOption } from './truncate_labels'; @@ -169,6 +170,12 @@ const PieOptions = (props: PieOptionsProps) => { }} data-test-subj="visTypePieNestedLegendSwitch" /> + )} {props.showElasticChartsOptions && palettesRegistry && ( @@ -276,7 +283,13 @@ const PieOptions = (props: PieOptionsProps) => { /> )} - + ); diff --git a/src/plugins/vis_type_pie/public/editor/components/truncate_labels.tsx b/src/plugins/vis_type_pie/public/editor/components/truncate_labels.tsx index e6eb56725753c..d4c798498e8b0 100644 --- a/src/plugins/vis_type_pie/public/editor/components/truncate_labels.tsx +++ b/src/plugins/vis_type_pie/public/editor/components/truncate_labels.tsx @@ -8,7 +8,7 @@ import React, { ChangeEvent } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFormRow, EuiFieldNumber } from '@elastic/eui'; +import { EuiFormRow, EuiFieldNumber, EuiIconTip } from '@elastic/eui'; export interface TruncateLabelsOptionProps { disabled?: boolean; @@ -27,6 +27,16 @@ function TruncateLabelsOption({ disabled, value = null, setValue }: TruncateLabe })} fullWidth display="rowCompressed" + labelAppend={ + + } > { }, legendPosition: 'right', nestedLegend: false, + maxLegendLines: 1, + truncateLegend: true, distinctColors: false, palette: { name: 'default', diff --git a/src/plugins/vis_type_pie/public/pie_component.tsx b/src/plugins/vis_type_pie/public/pie_component.tsx index c0f4a8a6112f8..9119f2f2ecd6c 100644 --- a/src/plugins/vis_type_pie/public/pie_component.tsx +++ b/src/plugins/vis_type_pie/public/pie_component.tsx @@ -320,7 +320,16 @@ const PieComponent = (props: PieComponentProps) => { services.actions, services.fieldFormats )} - theme={chartTheme} + theme={[ + chartTheme, + { + legend: { + labelOptions: { + maxLines: visParams.truncateLegend ? visParams.maxLegendLines ?? 1 : 0, + }, + }, + }, + ]} baseTheme={chartBaseTheme} onRenderChange={onRenderChange} /> diff --git a/src/plugins/vis_type_pie/public/pie_fn.test.ts b/src/plugins/vis_type_pie/public/pie_fn.test.ts index 3dcef406379c2..33b5f38cbe630 100644 --- a/src/plugins/vis_type_pie/public/pie_fn.test.ts +++ b/src/plugins/vis_type_pie/public/pie_fn.test.ts @@ -23,6 +23,8 @@ describe('interpreter/functions#pie', () => { legendPosition: 'right', isDonut: true, nestedLegend: true, + truncateLegend: true, + maxLegendLines: true, distinctColors: false, palette: 'kibana_palette', labels: { diff --git a/src/plugins/vis_type_pie/public/pie_fn.ts b/src/plugins/vis_type_pie/public/pie_fn.ts index 65ac648ca2868..c5987001d4494 100644 --- a/src/plugins/vis_type_pie/public/pie_fn.ts +++ b/src/plugins/vis_type_pie/public/pie_fn.ts @@ -89,6 +89,19 @@ export const createPieVisFn = (): VisTypePieExpressionFunctionDefinition => ({ }), default: false, }, + truncateLegend: { + types: ['boolean'], + help: i18n.translate('visTypePie.function.args.truncateLegendHelpText', { + defaultMessage: 'Defines if the legend items will be truncated or not', + }), + default: true, + }, + maxLegendLines: { + types: ['number'], + help: i18n.translate('visTypePie.function.args.maxLegendLinesHelpText', { + defaultMessage: 'Defines the number of lines per legend item', + }), + }, distinctColors: { types: ['boolean'], help: i18n.translate('visTypePie.function.args.distinctColorsHelpText', { diff --git a/src/plugins/vis_type_pie/public/sample_vis.test.mocks.ts b/src/plugins/vis_type_pie/public/sample_vis.test.mocks.ts index 41fa00bbe2386..26d9c526a8137 100644 --- a/src/plugins/vis_type_pie/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_type_pie/public/sample_vis.test.mocks.ts @@ -28,6 +28,8 @@ export const samplePieVis = { legendPosition: 'right', isDonut: true, nestedLegend: true, + truncateLegend: true, + maxLegendLines: 1, distinctColors: false, palette: 'kibana_palette', labels: { diff --git a/src/plugins/vis_type_pie/public/to_ast.ts b/src/plugins/vis_type_pie/public/to_ast.ts index e8c9f301b4366..b360e375bf40d 100644 --- a/src/plugins/vis_type_pie/public/to_ast.ts +++ b/src/plugins/vis_type_pie/public/to_ast.ts @@ -50,6 +50,8 @@ export const toExpressionAst: VisToExpressionAst = async (vis, par addLegend: vis.params.addLegend, legendPosition: vis.params.legendPosition, nestedLegend: vis.params?.nestedLegend, + truncateLegend: vis.params.truncateLegend, + maxLegendLines: vis.params.maxLegendLines, distinctColors: vis.params?.distinctColors, isDonut: vis.params.isDonut, palette: vis.params?.palette?.name, diff --git a/src/plugins/vis_type_pie/public/types/types.ts b/src/plugins/vis_type_pie/public/types/types.ts index 4f3365545d062..94eaeb55f7242 100644 --- a/src/plugins/vis_type_pie/public/types/types.ts +++ b/src/plugins/vis_type_pie/public/types/types.ts @@ -33,6 +33,8 @@ interface PieCommonParams { addLegend: boolean; legendPosition: Position; nestedLegend: boolean; + truncateLegend: boolean; + maxLegendLines: number; distinctColors: boolean; isDonut: boolean; } diff --git a/src/plugins/vis_type_pie/public/utils/get_columns.test.ts b/src/plugins/vis_type_pie/public/utils/get_columns.test.ts index 3170628ec2e12..9f64266ed0e0e 100644 --- a/src/plugins/vis_type_pie/public/utils/get_columns.test.ts +++ b/src/plugins/vis_type_pie/public/utils/get_columns.test.ts @@ -144,6 +144,8 @@ describe('getColumns', () => { }, legendPosition: 'right', nestedLegend: false, + maxLegendLines: 1, + truncateLegend: false, palette: { name: 'default', type: 'palette', diff --git a/src/plugins/vis_type_pie/public/utils/get_config.ts b/src/plugins/vis_type_pie/public/utils/get_config.ts index a8a4edb01cd9c..40f8f84b127f9 100644 --- a/src/plugins/vis_type_pie/public/utils/get_config.ts +++ b/src/plugins/vis_type_pie/public/utils/get_config.ts @@ -63,6 +63,7 @@ export const getConfig = ( config.linkLabel = { maxCount: Number.POSITIVE_INFINITY, maximumSection: Number.POSITIVE_INFINITY, + maxTextLength: visParams.labels.truncate ?? undefined, }; } diff --git a/src/plugins/vis_type_pie/public/utils/get_layers.ts b/src/plugins/vis_type_pie/public/utils/get_layers.ts index b995df83c0bb0..42c4650419c6b 100644 --- a/src/plugins/vis_type_pie/public/utils/get_layers.ts +++ b/src/plugins/vis_type_pie/public/utils/get_layers.ts @@ -151,12 +151,7 @@ export const getLayers = ( showAccessor: (d: Datum) => d !== EMPTY_SLICE, nodeLabel: (d: unknown) => { if (col.format) { - const formattedLabel = formatter.deserialize(col.format).convert(d) ?? ''; - if (visParams.labels.truncate && formattedLabel.length <= visParams.labels.truncate) { - return formattedLabel; - } else { - return `${formattedLabel.slice(0, Number(visParams.labels.truncate))}\u2026`; - } + return formatter.deserialize(col.format).convert(d) ?? ''; } return String(d); }, diff --git a/src/plugins/vis_type_pie/public/vis_type/pie.ts b/src/plugins/vis_type_pie/public/vis_type/pie.ts index 9d1556ac33ad7..95a9d0d41481b 100644 --- a/src/plugins/vis_type_pie/public/vis_type/pie.ts +++ b/src/plugins/vis_type_pie/public/vis_type/pie.ts @@ -35,6 +35,8 @@ export const getPieVisTypeDefinition = ({ addLegend: !showElasticChartsOptions, legendPosition: Position.Right, nestedLegend: false, + truncateLegend: true, + maxLegendLines: 1, distinctColors: false, isDonut: true, palette: { diff --git a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap index 8b720568c4d2c..233940d97d38a 100644 --- a/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_type_vislib/public/__snapshots__/to_ast.test.ts.snap @@ -8,7 +8,7 @@ Object { "area", ], "visConfig": Array [ - "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"circlesRadius\\":5,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"palette\\":{\\"name\\":\\"default\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", + "{\\"type\\":\\"area\\",\\"grid\\":{\\"categoryLines\\":false,\\"style\\":{\\"color\\":\\"#eee\\"}},\\"categoryAxes\\":[{\\"id\\":\\"CategoryAxis-1\\",\\"type\\":\\"category\\",\\"position\\":\\"bottom\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\"},\\"labels\\":{\\"show\\":true,\\"truncate\\":100},\\"title\\":{}}],\\"valueAxes\\":[{\\"id\\":\\"ValueAxis-1\\",\\"name\\":\\"LeftAxis-1\\",\\"type\\":\\"value\\",\\"position\\":\\"left\\",\\"show\\":true,\\"style\\":{},\\"scale\\":{\\"type\\":\\"linear\\",\\"mode\\":\\"normal\\"},\\"labels\\":{\\"show\\":true,\\"rotate\\":0,\\"filter\\":false,\\"truncate\\":100},\\"title\\":{\\"text\\":\\"Sum of total_quantity\\"}}],\\"seriesParams\\":[{\\"show\\":\\"true\\",\\"type\\":\\"area\\",\\"mode\\":\\"stacked\\",\\"data\\":{\\"label\\":\\"Sum of total_quantity\\",\\"id\\":\\"1\\"},\\"drawLinesBetweenPoints\\":true,\\"showCircles\\":true,\\"circlesRadius\\":5,\\"interpolate\\":\\"linear\\",\\"valueAxis\\":\\"ValueAxis-1\\"}],\\"addTooltip\\":true,\\"addLegend\\":true,\\"legendPosition\\":\\"top\\",\\"times\\":[],\\"addTimeMarker\\":false,\\"truncateLegend\\":true,\\"maxLegendLines\\":1,\\"thresholdLine\\":{\\"show\\":false,\\"value\\":10,\\"width\\":1,\\"style\\":\\"full\\",\\"color\\":\\"#E7664C\\"},\\"palette\\":{\\"name\\":\\"default\\"},\\"labels\\":{},\\"dimensions\\":{\\"x\\":{\\"accessor\\":1,\\"format\\":{\\"id\\":\\"date\\",\\"params\\":{\\"pattern\\":\\"HH:mm:ss.SSS\\"}},\\"params\\":{}},\\"y\\":[{\\"accessor\\":0,\\"format\\":{\\"id\\":\\"number\\",\\"params\\":{\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}],\\"series\\":[{\\"accessor\\":2,\\"format\\":{\\"id\\":\\"terms\\",\\"params\\":{\\"id\\":\\"string\\",\\"otherBucketLabel\\":\\"Other\\",\\"missingBucketLabel\\":\\"Missing\\",\\"parsedUrl\\":{\\"origin\\":\\"http://localhost:5801\\",\\"pathname\\":\\"/app/visualize\\",\\"basePath\\":\\"\\"}}},\\"params\\":{}}]}}", ], }, "getArgument": [Function], diff --git a/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap index 7c21e699216bc..7ee1b0d2b2053 100644 --- a/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap +++ b/src/plugins/vis_type_xy/public/__snapshots__/to_ast.test.ts.snap @@ -32,6 +32,9 @@ Object { "legendPosition": Array [ "top", ], + "maxLegendLines": Array [ + 1, + ], "palette": Array [ "default", ], @@ -51,6 +54,9 @@ Object { }, ], "times": Array [], + "truncateLegend": Array [ + true, + ], "type": Array [ "area", ], diff --git a/src/plugins/vis_type_xy/public/components/xy_settings.tsx b/src/plugins/vis_type_xy/public/components/xy_settings.tsx index 03455bae69506..2dd7d7e0a91f9 100644 --- a/src/plugins/vis_type_xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_type_xy/public/components/xy_settings.tsx @@ -60,6 +60,8 @@ type XYSettingsProps = Pick< legendAction?: LegendAction; legendColorPicker: LegendColorPicker; legendPosition: Position; + truncateLegend: boolean; + maxLegendLines: number; }; function getValueLabelsStyling() { @@ -93,6 +95,8 @@ export const XYSettings: FC = ({ legendAction, legendColorPicker, legendPosition, + maxLegendLines, + truncateLegend, }) => { const themeService = getThemeService(); const theme = themeService.useChartsTheme(); @@ -113,6 +117,9 @@ export const XYSettings: FC = ({ crosshair: { ...theme.crosshair, }, + legend: { + labelOptions: { maxLines: truncateLegend ? maxLegendLines ?? 1 : 0 }, + }, axes: { axisTitle: { padding: { diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts index d5e1360ced74c..e51b47bc4c7fa 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.mocks.ts @@ -426,6 +426,8 @@ export const getVis = (bucketType: string) => { fittingFunction: 'linear', times: [], addTimeMarker: false, + maxLegendLines: 1, + truncateLegend: true, radiusRatio: 9, thresholdLine: { show: false, @@ -849,6 +851,8 @@ export const getStateParams = (type: string, thresholdPanelOn: boolean) => { legendPosition: 'right', times: [], addTimeMarker: false, + maxLegendLines: 1, + truncateLegend: true, detailedTooltip: true, palette: { type: 'palette', diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.test.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.test.tsx index 59c03e02ac9f4..7fedd38e4e7ec 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.test.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.test.tsx @@ -105,6 +105,26 @@ describe('PointSeries Editor', function () { }); }); + it('not renders the long legend options if showElasticChartsOptions is false', async () => { + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'xyLongLegendsOptions').length).toBe(0); + }); + }); + + it('renders the long legend options if showElasticChartsOptions is true', async () => { + const newVisProps = ({ + ...props, + extraProps: { + showElasticChartsOptions: true, + }, + } as unknown) as PointSeriesOptionsProps; + component = mountWithIntl(); + await act(async () => { + expect(findTestSubject(component, 'xyLongLegendsOptions').length).toBe(1); + }); + }); + it('not renders the fitting function for a bar chart', async () => { const newVisProps = ({ ...props, diff --git a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx index 343976651d21e..1fd9b043e87f5 100644 --- a/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx +++ b/src/plugins/vis_type_xy/public/editor/components/options/point_series/point_series.tsx @@ -11,7 +11,11 @@ import { EuiPanel, EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { BasicOptions, SwitchOption } from '../../../../../../vis_default_editor/public'; +import { + BasicOptions, + SwitchOption, + LongLegendOptions, +} from '../../../../../../vis_default_editor/public'; import { BUCKET_TYPES } from '../../../../../../data/public'; import { VisParams } from '../../../../types'; @@ -58,6 +62,14 @@ export function PointSeriesOptions( + {props.extraProps?.showElasticChartsOptions && ( + + )} {vis.data.aggs!.aggs.some( (agg) => agg.schema === 'segment' && agg.type.name === BUCKET_TYPES.DATE_HISTOGRAM diff --git a/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts b/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts index 35f3b2d7c627d..6d2b860066b07 100644 --- a/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts +++ b/src/plugins/vis_type_xy/public/expression_functions/xy_vis_fn.ts @@ -55,6 +55,18 @@ export const visTypeXyVisFn = (): VisTypeXyExpressionFunctionDefinition => ({ defaultMessage: 'Show time marker', }), }, + truncateLegend: { + types: ['boolean'], + help: i18n.translate('visTypeXy.function.args.truncateLegend.help', { + defaultMessage: 'Defines if the legend will be truncated or not', + }), + }, + maxLegendLines: { + types: ['number'], + help: i18n.translate('visTypeXy.function.args.args.maxLegendLines.help', { + defaultMessage: 'Defines the maximum lines per legend item', + }), + }, addLegend: { types: ['boolean'], help: i18n.translate('visTypeXy.function.args.addLegend.help', { @@ -225,6 +237,8 @@ export const visTypeXyVisFn = (): VisTypeXyExpressionFunctionDefinition => ({ addTooltip: args.addTooltip, legendPosition: args.legendPosition, addTimeMarker: args.addTimeMarker, + maxLegendLines: args.maxLegendLines, + truncateLegend: args.truncateLegend, categoryAxes: args.categoryAxes.map((categoryAxis) => ({ ...categoryAxis, type: categoryAxis.axisType, diff --git a/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts b/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts index 8fafd4c723055..7fff29edfab51 100644 --- a/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts +++ b/src/plugins/vis_type_xy/public/sample_vis.test.mocks.ts @@ -88,6 +88,8 @@ export const sampleAreaVis = { legendPosition: 'right', times: [], addTimeMarker: false, + truncateLegend: true, + maxLegendLines: 1, thresholdLine: { show: false, value: 10, @@ -255,6 +257,8 @@ export const sampleAreaVis = { legendPosition: 'top', times: [], addTimeMarker: false, + truncateLegend: true, + maxLegendLines: 1, thresholdLine: { show: false, value: 10, diff --git a/src/plugins/vis_type_xy/public/to_ast.ts b/src/plugins/vis_type_xy/public/to_ast.ts index 9fec3f99ab39b..0b1eb5262d71a 100644 --- a/src/plugins/vis_type_xy/public/to_ast.ts +++ b/src/plugins/vis_type_xy/public/to_ast.ts @@ -194,6 +194,8 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params type: vis.type.name as XyVisType, chartType: vis.params.type, addTimeMarker: vis.params.addTimeMarker, + truncateLegend: vis.params.truncateLegend, + maxLegendLines: vis.params.maxLegendLines, addLegend: vis.params.addLegend, addTooltip: vis.params.addTooltip, legendPosition: vis.params.legendPosition, diff --git a/src/plugins/vis_type_xy/public/types/param.ts b/src/plugins/vis_type_xy/public/types/param.ts index 421690f7fc6a9..0687bd2af2cd1 100644 --- a/src/plugins/vis_type_xy/public/types/param.ts +++ b/src/plugins/vis_type_xy/public/types/param.ts @@ -121,6 +121,8 @@ export interface VisParams { addTooltip: boolean; legendPosition: Position; addTimeMarker: boolean; + truncateLegend: boolean; + maxLegendLines: number; categoryAxes: CategoryAxis[]; orderBucketsBySum?: boolean; labels: Labels; @@ -158,6 +160,8 @@ export interface XYVisConfig { addTooltip: boolean; legendPosition: Position; addTimeMarker: boolean; + truncateLegend: boolean; + maxLegendLines: number; orderBucketsBySum?: boolean; labels: ExpressionValueLabel; thresholdLine: ExpressionValueThresholdLine; diff --git a/src/plugins/vis_type_xy/public/vis_component.tsx b/src/plugins/vis_type_xy/public/vis_component.tsx index 2dffabb2ba0b9..346f6cc74a1ac 100644 --- a/src/plugins/vis_type_xy/public/vis_component.tsx +++ b/src/plugins/vis_type_xy/public/vis_component.tsx @@ -345,6 +345,8 @@ const VisComponent = (props: VisComponentProps) => { /> Date: Thu, 19 Aug 2021 12:27:42 +0100 Subject: [PATCH 24/28] chore(NA): moving @kbn/plugin-generator to babel transpiler (#109083) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-plugin-generator/.babelrc | 3 +++ packages/kbn-plugin-generator/BUILD.bazel | 23 +++++++++++++++------ packages/kbn-plugin-generator/package.json | 4 ++-- packages/kbn-plugin-generator/tsconfig.json | 5 +++-- 4 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 packages/kbn-plugin-generator/.babelrc diff --git a/packages/kbn-plugin-generator/.babelrc b/packages/kbn-plugin-generator/.babelrc new file mode 100644 index 0000000000000..7da72d1779128 --- /dev/null +++ b/packages/kbn-plugin-generator/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"] +} diff --git a/packages/kbn-plugin-generator/BUILD.bazel b/packages/kbn-plugin-generator/BUILD.bazel index c16862ee4f3c2..c935d1763dae8 100644 --- a/packages/kbn-plugin-generator/BUILD.bazel +++ b/packages/kbn-plugin-generator/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-plugin-generator" PKG_REQUIRE_NAME = "@kbn/plugin-generator" @@ -35,7 +36,7 @@ NPM_MODULE_EXTRA_FILES = [ ":template", ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-utils", "//packages/kbn-dev-utils", "@npm//del", @@ -49,6 +50,11 @@ SRC_DEPS = [ ] TYPES_DEPS = [ + "//packages/kbn-utils", + "//packages/kbn-dev-utils", + "@npm//del", + "@npm//execa", + "@npm//globby", "@npm//@types/ejs", "@npm//@types/inquirer", "@npm//@types/jest", @@ -58,7 +64,11 @@ TYPES_DEPS = [ "@npm//@types/vinyl-fs", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) ts_config( name = "tsconfig", @@ -70,13 +80,14 @@ ts_config( ) ts_project( - name = "tsc", + name = "tsc_types", args = ['--pretty'], srcs = SRCS, - deps = DEPS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - out_dir = "target", + emit_declaration_only = True, + out_dir = "target_types", source_map = True, root_dir = "src", tsconfig = ":tsconfig", @@ -85,7 +96,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = DEPS + [":tsc"], + deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) diff --git a/packages/kbn-plugin-generator/package.json b/packages/kbn-plugin-generator/package.json index 298373afd2f24..d30f25e478dcf 100644 --- a/packages/kbn-plugin-generator/package.json +++ b/packages/kbn-plugin-generator/package.json @@ -3,6 +3,6 @@ "version": "1.0.0", "private": true, "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "target/index.js", - "types": "target/index.d.ts" + "main": "target_node/index.js", + "types": "target_types/index.d.ts" } \ No newline at end of file diff --git a/packages/kbn-plugin-generator/tsconfig.json b/packages/kbn-plugin-generator/tsconfig.json index 6a25803c83940..5b666cf801da6 100644 --- a/packages/kbn-plugin-generator/tsconfig.json +++ b/packages/kbn-plugin-generator/tsconfig.json @@ -1,12 +1,13 @@ { "extends": "../../tsconfig.bazel.json", "compilerOptions": { - "outDir": "target", - "target": "ES2019", "declaration": true, "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "target_types", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-plugin-generator/src", + "target": "ES2019", "types": [ "jest", "node" From 6c1133ad7c8c4827fe6c63e4226f9c10b602dd8a Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Thu, 19 Aug 2021 13:42:06 +0200 Subject: [PATCH 25/28] [ML] Fix date formatting in the alert context of the Anomaly detection job health rule type (#109073) * [ML] extend alert messages with job ids * [ML] rename getJobsErrorMessages * [ML] add rule name to a template * [ML] utilize fieldFormats plugin for date formatting * [ML] update types * [ML] update unit tests * [ML] guards for uiSettings and fieldRegistry, add custom errors * [ML] add conditional call for uiSettings * [ML] fix i18n * [ML] tests for getCustomError * [ML] fix i18n check * [ML] create classes for custom errors --- x-pack/plugins/ml/common/types/kibana.ts | 7 +- x-pack/plugins/ml/kibana.json | 3 +- .../register_jobs_health_alerting_rule.ts | 2 +- .../lib/alerts/jobs_health_service.test.ts | 34 ++-- .../server/lib/alerts/jobs_health_service.ts | 150 +++++++++++++----- .../register_jobs_monitoring_rule_type.ts | 15 +- .../job_audit_messages/job_audit_messages.ts | 7 +- x-pack/plugins/ml/server/plugin.ts | 10 +- .../ml/server/shared_services/errors.test.ts | 48 ++++++ .../ml/server/shared_services/errors.ts | 29 +++- .../server/shared_services/shared_services.ts | 68 ++++++-- x-pack/plugins/ml/server/types.ts | 6 + 12 files changed, 305 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/ml/server/shared_services/errors.test.ts diff --git a/x-pack/plugins/ml/common/types/kibana.ts b/x-pack/plugins/ml/common/types/kibana.ts index cbdc09fa01be3..7783a02c2dd37 100644 --- a/x-pack/plugins/ml/common/types/kibana.ts +++ b/x-pack/plugins/ml/common/types/kibana.ts @@ -7,8 +7,9 @@ // custom edits or fixes for default kibana types which are incomplete -import { SimpleSavedObject } from 'kibana/public'; -import { IndexPatternAttributes } from 'src/plugins/data/common'; +import type { SimpleSavedObject } from 'kibana/public'; +import type { IndexPatternAttributes } from 'src/plugins/data/common'; +import type { FieldFormatsRegistry } from '../../../../../src/plugins/field_formats/common'; export type IndexPatternTitle = string; @@ -26,3 +27,5 @@ export function isSavedSearchSavedObject( ): ss is SavedSearchSavedObject { return ss !== null; } + +export type FieldFormatsRegistryProvider = () => Promise; diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json index 1196247fe4629..310ac5d65c986 100644 --- a/x-pack/plugins/ml/kibana.json +++ b/x-pack/plugins/ml/kibana.json @@ -17,7 +17,8 @@ "uiActions", "kibanaLegacy", "discover", - "triggersActionsUi" + "triggersActionsUi", + "fieldFormats" ], "optionalPlugins": [ "alerting", diff --git a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts index a5f433bcc3752..68a06919d03a3 100644 --- a/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts +++ b/x-pack/plugins/ml/public/alerting/jobs_health_rule/register_jobs_health_alerting_rule.ts @@ -87,7 +87,7 @@ export function registerJobsHealthAlertingRule( defaultActionMessage: i18n.translate( 'xpack.ml.alertTypes.jobsHealthAlertingRule.defaultActionMessage', { - defaultMessage: `Anomaly detection jobs health check result: + defaultMessage: `[\\{\\{rule.name\\}\\}] Anomaly detection jobs health check result: \\{\\{context.message\\}\\} \\{\\{#context.results\\}\\} Job ID: \\{\\{job_id\\}\\} diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts index ffaa26fc949ee..7192b9a919379 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.test.ts @@ -14,6 +14,7 @@ import { AnnotationService } from '../../models/annotation_service/annotation'; import { JobsHealthExecutorOptions } from './register_jobs_monitoring_rule_type'; import { JobAuditMessagesService } from '../../models/job_audit_messages/job_audit_messages'; import { DeepPartial } from '../../../common/types/common'; +import { FieldFormatsRegistryProvider } from '../../../common/types/kibana'; const MOCK_DATE_NOW = 1487076708000; @@ -148,8 +149,8 @@ describe('JobsHealthService', () => { } as unknown) as jest.Mocked; const jobAuditMessagesService = ({ - getJobsErrors: jest.fn().mockImplementation((jobIds: string) => { - return Promise.resolve({}); + getJobsErrorMessages: jest.fn().mockImplementation((jobIds: string) => { + return Promise.resolve([]); }), } as unknown) as jest.Mocked; @@ -159,11 +160,24 @@ describe('JobsHealthService', () => { debug: jest.fn(), } as unknown) as jest.Mocked; + const getFieldsFormatRegistry = jest.fn().mockImplementation(() => { + return Promise.resolve({ + deserialize: jest.fn().mockImplementation(() => { + return { + convert: jest.fn().mockImplementation((v) => { + return new Date(v).toUTCString(); + }), + }; + }), + }); + }) as jest.Mocked; + const jobHealthService: JobsHealthService = jobsHealthServiceProvider( mlClient, datafeedsService, annotationService, jobAuditMessagesService, + getFieldsFormatRegistry, logger ); @@ -275,11 +289,11 @@ describe('JobsHealthService', () => { job_id: 'test_job_01', annotation: 'Datafeed has missed 11 documents due to ingest latency, latest bucket with missing data is [2021-07-30T13:50:00.000Z]. Consider increasing query_delay', - end_timestamp: 1627653300000, + end_timestamp: 'Fri, 30 Jul 2021 13:55:00 GMT', missed_docs_count: 11, }, ], - message: '1 job is suffering from delayed data.', + message: 'Job test_job_01 is suffering from delayed data.', }, }, ]); @@ -333,7 +347,7 @@ describe('JobsHealthService', () => { datafeed_state: 'stopped', }, ], - message: 'Datafeed is not started for the following jobs:', + message: 'Datafeed is not started for job test_job_02', }, }, { @@ -342,12 +356,12 @@ describe('JobsHealthService', () => { results: [ { job_id: 'test_job_01', - log_time: 1626935914540, + log_time: 'Thu, 22 Jul 2021 06:38:34 GMT', memory_status: 'hard_limit', }, ], message: - '1 job reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.', + 'Job test_job_01 reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.', }, }, { @@ -358,18 +372,18 @@ describe('JobsHealthService', () => { job_id: 'test_job_01', annotation: 'Datafeed has missed 11 documents due to ingest latency, latest bucket with missing data is [2021-07-30T13:50:00.000Z]. Consider increasing query_delay', - end_timestamp: 1627653300000, + end_timestamp: 'Fri, 30 Jul 2021 13:55:00 GMT', missed_docs_count: 11, }, { job_id: 'test_job_02', annotation: 'Datafeed has missed 8 documents due to ingest latency, latest bucket with missing data is [2021-07-30T13:50:00.000Z]. Consider increasing query_delay', - end_timestamp: 1627653300000, + end_timestamp: 'Fri, 30 Jul 2021 13:55:00 GMT', missed_docs_count: 8, }, ], - message: '2 jobs are suffering from delayed data.', + message: 'Jobs test_job_01, test_job_02 are suffering from delayed data.', }, }, ]); diff --git a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts index bcae57e558573..ca63031f02e27 100644 --- a/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts +++ b/x-pack/plugins/ml/server/lib/alerts/jobs_health_service.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { memoize, keyBy } from 'lodash'; -import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { groupBy, keyBy, memoize } from 'lodash'; +import { KibanaRequest, Logger, SavedObjectsClientContract } from 'kibana/server'; import { i18n } from '@kbn/i18n'; -import { Logger } from 'kibana/server'; import { MlJob } from '@elastic/elasticsearch/api/types'; import { MlClient } from '../ml_client'; import { JobSelection } from '../../routes/schemas/alerting_schema'; @@ -19,6 +18,7 @@ import { GetGuards } from '../../shared_services/shared_services'; import { AnomalyDetectionJobsHealthAlertContext, DelayedDataResponse, + JobsErrorsResponse, JobsHealthExecutorOptions, MmlTestResponse, NotStartedDatafeedResponse, @@ -35,6 +35,7 @@ import { jobAuditMessagesProvider, JobAuditMessagesService, } from '../../models/job_audit_messages/job_audit_messages'; +import type { FieldFormatsRegistryProvider } from '../../../common/types/kibana'; interface TestResult { name: string; @@ -48,8 +49,18 @@ export function jobsHealthServiceProvider( datafeedsService: DatafeedsService, annotationService: AnnotationService, jobAuditMessagesService: JobAuditMessagesService, + getFieldsFormatRegistry: FieldFormatsRegistryProvider, logger: Logger ) { + /** + * Provides a callback for date formatting based on the Kibana settings. + */ + const getDateFormatter = memoize(async () => { + const fieldFormatsRegistry = await getFieldsFormatRegistry(); + const dateFormatter = fieldFormatsRegistry.deserialize({ id: 'date' }); + return dateFormatter.convert.bind(dateFormatter); + }); + /** * Extracts result list of jobs based on included and excluded selection of jobs and groups. * @param includeJobs @@ -121,6 +132,17 @@ export function jobsHealthServiceProvider( async (jobIds: string[]) => (await mlClient.getJobStats({ job_id: jobIds.join(',') })).body.jobs ); + /** Gets values for translation string */ + const getJobsAlertingMessageValues = >( + results: T + ) => { + const jobIds = (results || []).filter(isDefined).map((v) => v.job_id); + return { + count: jobIds.length, + jobsString: jobIds.join(', '), + }; + }; + return { /** * Gets not started datafeeds for opened jobs. @@ -164,13 +186,15 @@ export function jobsHealthServiceProvider( async getMmlReport(jobIds: string[]): Promise { const jobsStats = await getJobStats(jobIds); + const dateFormatter = await getDateFormatter(); + return jobsStats .filter((j) => j.state === 'opened' && j.model_size_stats.memory_status !== 'ok') .map(({ job_id: jobId, model_size_stats: modelSizeStats }) => { return { job_id: jobId, memory_status: modelSizeStats.memory_status, - log_time: modelSizeStats.log_time, + log_time: dateFormatter(modelSizeStats.log_time), model_bytes: modelSizeStats.model_bytes, model_bytes_memory_limit: modelSizeStats.model_bytes_memory_limit, peak_model_bytes: modelSizeStats.peak_model_bytes, @@ -203,13 +227,15 @@ export function jobsHealthServiceProvider( const defaultLookbackInterval = resolveLookbackInterval(resultJobs, datafeeds!); const earliestMs = getDelayedDataLookbackTimestamp(timeInterval, defaultLookbackInterval); - const annotations: DelayedDataResponse[] = ( + const getFormattedDate = await getDateFormatter(); + + return ( await annotationService.getDelayedDataAnnotations({ jobIds: resultJobIds, earliestMs, }) ) - .map((v) => { + .map((v) => { const match = v.annotation.match(/Datafeed has missed (\d+)\s/); const missedDocsCount = match ? parseInt(match[1], 10) : 0; return { @@ -235,9 +261,13 @@ export function jobsHealthServiceProvider( v.end_timestamp > getDelayedDataLookbackTimestamp(timeInterval, jobLookbackInterval); return isDocCountExceededThreshold && isEndTimestampWithinRange; + }) + .map((v) => { + return { + ...v, + end_timestamp: getFormattedDate(v.end_timestamp), + }; }); - - return annotations; }, /** * Retrieves a list of the latest errors per jobs. @@ -245,8 +275,25 @@ export function jobsHealthServiceProvider( * @param previousStartedAt Time of the previous rule execution. As we intend to notify * about an error only once, limit the scope of the errors search. */ - async getErrorsReport(jobIds: string[], previousStartedAt: Date) { - return await jobAuditMessagesService.getJobsErrors(jobIds, previousStartedAt.getTime()); + async getErrorsReport( + jobIds: string[], + previousStartedAt: Date + ): Promise { + const getFormattedDate = await getDateFormatter(); + + return ( + await jobAuditMessagesService.getJobsErrorMessages(jobIds, previousStartedAt.getTime()) + ).map((v) => { + return { + ...v, + errors: v.errors.map((e) => { + return { + ...e, + timestamp: getFormattedDate(e.timestamp), + }; + }), + }; + }); }, /** * Retrieves report grouped by test. @@ -275,6 +322,7 @@ export function jobsHealthServiceProvider( if (config.datafeed.enabled) { const response = await this.getNotStartedDatafeeds(jobIds); if (response && response.length > 0) { + const { count, jobsString } = getJobsAlertingMessageValues(response); results.push({ name: HEALTH_CHECK_NAMES.datafeed.name, context: { @@ -282,7 +330,9 @@ export function jobsHealthServiceProvider( message: i18n.translate( 'xpack.ml.alertTypes.jobsHealthAlertingRule.datafeedStateMessage', { - defaultMessage: 'Datafeed is not started for the following jobs:', + defaultMessage: + 'Datafeed is not started for {count, plural, one {job} other {jobs}} {jobsString}', + values: { count, jobsString }, } ), }, @@ -293,32 +343,54 @@ export function jobsHealthServiceProvider( if (config.mml.enabled) { const response = await this.getMmlReport(jobIds); if (response && response.length > 0) { - const hardLimitJobsCount = response.reduce((acc, curr) => { - return acc + (curr.memory_status === 'hard_limit' ? 1 : 0); - }, 0); + const { hard_limit: hardLimitJobs, soft_limit: softLimitJobs } = groupBy( + response, + 'memory_status' + ); + + const { + count: hardLimitCount, + jobsString: hardLimitJobsString, + } = getJobsAlertingMessageValues(hardLimitJobs); + const { + count: softLimitCount, + jobsString: softLimitJobsString, + } = getJobsAlertingMessageValues(softLimitJobs); + + let message = ''; + + if (hardLimitCount > 0) { + message = i18n.translate('xpack.ml.alertTypes.jobsHealthAlertingRule.mmlMessage', { + defaultMessage: `{count, plural, one {Job} other {Jobs}} {jobsString} reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.`, + values: { + count: hardLimitCount, + jobsString: hardLimitJobsString, + }, + }); + } + + if (softLimitCount > 0) { + if (message.length > 0) { + message += '\n'; + } + message += i18n.translate( + 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlSoftLimitMessage', + { + defaultMessage: + '{count, plural, one {Job} other {Jobs}} {jobsString} reached the soft model memory limit. Assign the job more memory or edit the datafeed filter to limit scope of analysis.', + values: { + count: softLimitCount, + jobsString: softLimitJobsString, + }, + } + ); + } results.push({ name: HEALTH_CHECK_NAMES.mml.name, context: { results: response, - message: - hardLimitJobsCount > 0 - ? i18n.translate( - 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlHardLimitMessage', - { - defaultMessage: - '{jobsCount, plural, one {# job} other {# jobs}} reached the hard model memory limit. Assign the job more memory and restore from a snapshot from prior to reaching the hard limit.', - values: { jobsCount: hardLimitJobsCount }, - } - ) - : i18n.translate( - 'xpack.ml.alertTypes.jobsHealthAlertingRule.mmlSoftLimitMessage', - { - defaultMessage: - '{jobsCount, plural, one {# job} other {# jobs}} reached the soft model memory limit. Assign the job more memory or edit the datafeed filter to limit scope of analysis.', - values: { jobsCount: response.length }, - } - ), + message, }, }); } @@ -331,6 +403,8 @@ export function jobsHealthServiceProvider( config.delayedData.docsCount ); + const { count, jobsString } = getJobsAlertingMessageValues(response); + if (response.length > 0) { results.push({ name: HEALTH_CHECK_NAMES.delayedData.name, @@ -340,8 +414,8 @@ export function jobsHealthServiceProvider( 'xpack.ml.alertTypes.jobsHealthAlertingRule.delayedDataMessage', { defaultMessage: - '{jobsCount, plural, one {# job is} other {# jobs are}} suffering from delayed data.', - values: { jobsCount: response.length }, + '{count, plural, one {Job} other {Jobs}} {jobsString} {count, plural, one {is} other {are}} suffering from delayed data.', + values: { count, jobsString }, } ), }, @@ -352,6 +426,7 @@ export function jobsHealthServiceProvider( if (config.errorMessages.enabled && previousStartedAt) { const response = await this.getErrorsReport(jobIds, previousStartedAt); if (response.length > 0) { + const { count, jobsString } = getJobsAlertingMessageValues(response); results.push({ name: HEALTH_CHECK_NAMES.errorMessages.name, context: { @@ -360,8 +435,8 @@ export function jobsHealthServiceProvider( 'xpack.ml.alertTypes.jobsHealthAlertingRule.errorMessagesMessage', { defaultMessage: - '{jobsCount, plural, one {# job contains} other {# jobs contain}} errors in the messages.', - values: { jobsCount: response.length }, + '{count, plural, one {Job} other {Jobs}} {jobsString} {count, plural, one {contains} other {contain}} errors in the messages.', + values: { count, jobsString }, } ), }, @@ -390,12 +465,13 @@ export function getJobsHealthServiceProvider(getGuards: GetGuards) { return await getGuards(request, savedObjectsClient) .isFullLicense() .hasMlCapabilities(['canGetJobs']) - .ok(({ mlClient, scopedClient }) => + .ok(({ mlClient, scopedClient, getFieldsFormatRegistry }) => jobsHealthServiceProvider( mlClient, datafeedsProvider(scopedClient, mlClient), annotationServiceProvider(scopedClient), jobAuditMessagesProvider(scopedClient, mlClient), + getFieldsFormatRegistry, logger ).getTestsResults(...args) ); diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index c49c169d3bd21..4844bf1a94707 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -22,8 +22,8 @@ import { AlertInstanceState, AlertTypeState, } from '../../../../alerting/common'; -import { JobsErrorsResponse } from '../../models/job_audit_messages/job_audit_messages'; -import { AlertExecutorOptions } from '../../../../alerting/server'; +import type { AlertExecutorOptions } from '../../../../alerting/server'; +import type { JobMessage } from '../../../common/types/audit_message'; type ModelSizeStats = MlJobStats['model_size_stats']; @@ -51,14 +51,19 @@ export interface DelayedDataResponse { /** Number of missed documents */ missed_docs_count: number; /** Timestamp of the latest finalized bucket with missing docs */ - end_timestamp: number; + end_timestamp: string; +} + +export interface JobsErrorsResponse { + job_id: string; + errors: Array & { timestamp: string }>; } export type AnomalyDetectionJobHealthResult = | MmlTestResponse | NotStartedDatafeedResponse | DelayedDataResponse - | JobsErrorsResponse[number]; + | JobsErrorsResponse; export type AnomalyDetectionJobsHealthAlertContext = { results: AnomalyDetectionJobHealthResult[]; @@ -143,7 +148,7 @@ export function registerJobsMonitoringRuleType({ const executionResult = await getTestsResults(options); if (executionResult.length > 0) { - logger.info( + logger.debug( `"${name}" rule is scheduling actions for tests: ${executionResult .map((v) => v.name) .join(', ')}` diff --git a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts index fcda1a2a3ea73..69f5c8b36f10c 100644 --- a/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts +++ b/x-pack/plugins/ml/server/models/job_audit_messages/job_audit_messages.ts @@ -411,7 +411,10 @@ export function jobAuditMessagesProvider( * Retrieve list of errors per job. * @param jobIds */ - async function getJobsErrors(jobIds: string[], earliestMs?: number): Promise { + async function getJobsErrorMessages( + jobIds: string[], + earliestMs?: number + ): Promise { const { body } = await asInternalUser.search({ index: ML_NOTIFICATION_INDEX_PATTERN, ignore_unavailable: true, @@ -471,6 +474,6 @@ export function jobAuditMessagesProvider( getJobAuditMessages, getAuditMessagesSummary, clearJobAuditMessages, - getJobsErrors, + getJobsErrorMessages, }; } diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 35f66e86b955a..4dea3cc072ca5 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -17,6 +17,7 @@ import { IClusterClient, SavedObjectsServiceStart, SharedGlobalConfig, + UiSettingsServiceStart, } from 'kibana/server'; import type { SecurityPluginSetup } from '../../security/server'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; @@ -60,6 +61,7 @@ import { registerMlAlerts } from './lib/alerts/register_ml_alerts'; import { ML_ALERT_TYPES } from '../common/constants/alerts'; import { alertingRoutes } from './routes/alerting'; import { registerCollector } from './usage'; +import { FieldFormatsStart } from '../../../../src/plugins/field_formats/server'; export type MlPluginSetup = SharedServices; export type MlPluginStart = void; @@ -70,6 +72,8 @@ export class MlServerPlugin private mlLicense: MlLicense; private capabilities: CapabilitiesStart | null = null; private clusterClient: IClusterClient | null = null; + private fieldsFormat: FieldFormatsStart | null = null; + private uiSettings: UiSettingsServiceStart | null = null; private savedObjectsStart: SavedObjectsServiceStart | null = null; private spacesPlugin: SpacesPluginSetup | undefined; private security: SecurityPluginSetup | undefined; @@ -204,6 +208,8 @@ export class MlServerPlugin resolveMlCapabilities, () => this.clusterClient, () => getInternalSavedObjectsClient(), + () => this.uiSettings, + () => this.fieldsFormat, () => this.isMlReady ); @@ -223,7 +229,9 @@ export class MlServerPlugin return sharedServicesProviders; } - public start(coreStart: CoreStart): MlPluginStart { + public start(coreStart: CoreStart, plugins: PluginsStart): MlPluginStart { + this.uiSettings = coreStart.uiSettings; + this.fieldsFormat = plugins.fieldFormats; this.capabilities = coreStart.capabilities; this.clusterClient = coreStart.elasticsearch.client; this.savedObjectsStart = coreStart.savedObjects; diff --git a/x-pack/plugins/ml/server/shared_services/errors.test.ts b/x-pack/plugins/ml/server/shared_services/errors.test.ts new file mode 100644 index 0000000000000..727012595dff3 --- /dev/null +++ b/x-pack/plugins/ml/server/shared_services/errors.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + getCustomErrorClass, + MLClusterClientUninitialized, + MLUISettingsClientUninitialized, + MLFieldFormatRegistryUninitialized, +} from './errors'; + +describe('Custom errors', () => { + test('creates a custom error instance', () => { + const MLCustomError = getCustomErrorClass('MLCustomError'); + const errorInstance = new MLCustomError('farequote is not defined'); + expect(errorInstance.message).toBe('farequote is not defined'); + expect(errorInstance.name).toBe('MLCustomError'); + expect(errorInstance).toBeInstanceOf(MLCustomError); + // make sure that custom class extends Error + expect(errorInstance).toBeInstanceOf(Error); + }); + + test('MLClusterClientUninitialized', () => { + const errorInstance = new MLClusterClientUninitialized('cluster client is not initialized'); + expect(errorInstance.message).toBe('cluster client is not initialized'); + expect(errorInstance.name).toBe('MLClusterClientUninitialized'); + expect(errorInstance).toBeInstanceOf(MLClusterClientUninitialized); + }); + + test('MLUISettingsClientUninitialized', () => { + const errorInstance = new MLUISettingsClientUninitialized('cluster client is not initialized'); + expect(errorInstance.message).toBe('cluster client is not initialized'); + expect(errorInstance.name).toBe('MLUISettingsClientUninitialized'); + expect(errorInstance).toBeInstanceOf(MLUISettingsClientUninitialized); + }); + + test('MLFieldFormatRegistryUninitialized', () => { + const errorInstance = new MLFieldFormatRegistryUninitialized( + 'cluster client is not initialized' + ); + expect(errorInstance.message).toBe('cluster client is not initialized'); + expect(errorInstance.name).toBe('MLFieldFormatRegistryUninitialized'); + expect(errorInstance).toBeInstanceOf(MLFieldFormatRegistryUninitialized); + }); +}); diff --git a/x-pack/plugins/ml/server/shared_services/errors.ts b/x-pack/plugins/ml/server/shared_services/errors.ts index 4b8e6625c5aef..39c629ad50f5f 100644 --- a/x-pack/plugins/ml/server/shared_services/errors.ts +++ b/x-pack/plugins/ml/server/shared_services/errors.ts @@ -5,9 +5,26 @@ * 2.0. */ -export class MLClusterClientUninitialized extends Error { - constructor(message?: string) { - super(message); - Object.setPrototypeOf(this, new.target.prototype); - } -} +export const getCustomErrorClass = (className: string) => { + const CustomError = class extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + // Override the error instance name + Object.defineProperty(this, 'name', { value: className }); + } + }; + // set class name dynamically + Object.defineProperty(CustomError, 'name', { value: className }); + return CustomError; +}; + +export const MLClusterClientUninitialized = getCustomErrorClass('MLClusterClientUninitialized'); + +export const MLUISettingsClientUninitialized = getCustomErrorClass( + 'MLUISettingsClientUninitialized' +); + +export const MLFieldFormatRegistryUninitialized = getCustomErrorClass( + 'MLFieldFormatRegistryUninitialized' +); diff --git a/x-pack/plugins/ml/server/shared_services/shared_services.ts b/x-pack/plugins/ml/server/shared_services/shared_services.ts index 3766a48b0537d..5c8bbffe10aed 100644 --- a/x-pack/plugins/ml/server/shared_services/shared_services.ts +++ b/x-pack/plugins/ml/server/shared_services/shared_services.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { IClusterClient, IScopedClusterClient, SavedObjectsClientContract } from 'kibana/server'; +import { + IClusterClient, + IScopedClusterClient, + SavedObjectsClientContract, + UiSettingsServiceStart, +} from 'kibana/server'; import { SpacesPluginStart } from '../../../spaces/server'; import { KibanaRequest } from '../../.././../../src/core/server'; import { MlLicense } from '../../common/license'; @@ -23,7 +28,11 @@ import { } from './providers/anomaly_detectors'; import { ResolveMlCapabilities, MlCapabilitiesKey } from '../../common/types/capabilities'; import { hasMlCapabilitiesProvider, HasMlCapabilities } from '../lib/capabilities'; -import { MLClusterClientUninitialized } from './errors'; +import { + MLClusterClientUninitialized, + MLFieldFormatRegistryUninitialized, + MLUISettingsClientUninitialized, +} from './errors'; import { MlClient, getMlClient } from '../lib/ml_client'; import { jobSavedObjectServiceFactory, JobSavedObjectService } from '../saved_objects'; import { @@ -34,6 +43,8 @@ import { getJobsHealthServiceProvider, JobsHealthServiceProvider, } from '../lib/alerts/jobs_health_service'; +import type { FieldFormatsStart } from '../../../../../src/plugins/field_formats/server'; +import type { FieldFormatsRegistryProvider } from '../../common/types/kibana'; export type SharedServices = JobServiceProvider & AnomalyDetectorsProvider & @@ -64,6 +75,7 @@ interface OkParams { scopedClient: IScopedClusterClient; mlClient: MlClient; jobSavedObjectService: JobSavedObjectService; + getFieldsFormatRegistry: FieldFormatsRegistryProvider; } type OkCallback = (okParams: OkParams) => any; @@ -76,6 +88,8 @@ export function createSharedServices( resolveMlCapabilities: ResolveMlCapabilities, getClusterClient: () => IClusterClient | null, getInternalSavedObjectsClient: () => SavedObjectsClientContract | null, + getUiSettings: () => UiSettingsServiceStart | null, + getFieldsFormat: () => FieldFormatsStart | null, isMlReady: () => Promise ): { sharedServicesProviders: SharedServices; @@ -97,12 +111,18 @@ export function createSharedServices( internalSavedObjectsClient, authorization, getSpaces !== undefined, - isMlReady + isMlReady, + getUiSettings, + getFieldsFormat ); - const { hasMlCapabilities, scopedClient, mlClient, jobSavedObjectService } = getRequestItems( - request - ); + const { + hasMlCapabilities, + scopedClient, + mlClient, + jobSavedObjectService, + getFieldsFormatRegistry, + } = getRequestItems(request); const asyncGuards: Array> = []; const guards: Guards = { @@ -120,7 +140,7 @@ export function createSharedServices( }, async ok(callback: OkCallback) { await Promise.all(asyncGuards); - return callback({ scopedClient, mlClient, jobSavedObjectService }); + return callback({ scopedClient, mlClient, jobSavedObjectService, getFieldsFormatRegistry }); }, }; return guards; @@ -154,7 +174,9 @@ function getRequestItemsProvider( internalSavedObjectsClient: SavedObjectsClientContract, authorization: SecurityPluginSetup['authz'] | undefined, spaceEnabled: boolean, - isMlReady: () => Promise + isMlReady: () => Promise, + getUiSettings: () => UiSettingsServiceStart | null, + getFieldsFormat: () => FieldFormatsStart | null ) { return (request: KibanaRequest) => { const getHasMlCapabilities = hasMlCapabilitiesProvider(resolveMlCapabilities); @@ -177,6 +199,28 @@ function getRequestItemsProvider( throw new MLClusterClientUninitialized(`ML's cluster client has not been initialized`); } + const uiSettingsClient = getUiSettings()?.asScopedToClient(savedObjectsClient); + if (!uiSettingsClient) { + throw new MLUISettingsClientUninitialized(`ML's UI settings client has not been initialized`); + } + + const getFieldsFormatRegistry = async () => { + let fieldFormatRegistry; + try { + fieldFormatRegistry = await getFieldsFormat()!.fieldFormatServiceFactory(uiSettingsClient!); + } catch (e) { + // throw an custom error during the fieldFormatRegistry check + } + + if (!fieldFormatRegistry) { + throw new MLFieldFormatRegistryUninitialized( + `ML's field format registry has not been initialized` + ); + } + + return fieldFormatRegistry; + }; + if (request instanceof KibanaRequest) { hasMlCapabilities = getHasMlCapabilities(request); scopedClient = clusterClient.asScoped(request); @@ -190,6 +234,12 @@ function getRequestItemsProvider( }; mlClient = getMlClient(scopedClient, jobSavedObjectService); } - return { hasMlCapabilities, scopedClient, mlClient, jobSavedObjectService }; + return { + hasMlCapabilities, + scopedClient, + mlClient, + jobSavedObjectService, + getFieldsFormatRegistry, + }; }; } diff --git a/x-pack/plugins/ml/server/types.ts b/x-pack/plugins/ml/server/types.ts index b04b8d8601772..da83b03766af4 100644 --- a/x-pack/plugins/ml/server/types.ts +++ b/x-pack/plugins/ml/server/types.ts @@ -23,6 +23,10 @@ import type { PluginSetup as DataPluginSetup, PluginStart as DataPluginStart, } from '../../../../src/plugins/data/server'; +import type { + FieldFormatsSetup, + FieldFormatsStart, +} from '../../../../src/plugins/field_formats/server'; export interface LicenseCheckResult { isAvailable: boolean; @@ -47,6 +51,7 @@ export interface SavedObjectsRouteDeps { export interface PluginsSetup { cloud: CloudSetup; data: DataPluginSetup; + fieldFormats: FieldFormatsSetup; features: FeaturesPluginSetup; home: HomeServerPluginSetup; licensing: LicensingPluginSetup; @@ -59,6 +64,7 @@ export interface PluginsSetup { export interface PluginsStart { data: DataPluginStart; + fieldFormats: FieldFormatsStart; spaces?: SpacesPluginStart; } From 3cb3984d590daad107acf0731326434562636bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Fern=C3=A1ndez=20Haro?= Date: Thu, 19 Aug 2021 13:40:56 +0100 Subject: [PATCH 26/28] [Flaky-tests] Fix SOM edit-saved-objects test (#109215) --- .../apps/saved_objects_management/edit_saved_object.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/functional/apps/saved_objects_management/edit_saved_object.ts b/test/functional/apps/saved_objects_management/edit_saved_object.ts index a90e927416685..f4bf45c0b7f70 100644 --- a/test/functional/apps/saved_objects_management/edit_saved_object.ts +++ b/test/functional/apps/saved_objects_management/edit_saved_object.ts @@ -53,6 +53,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await button.focus(); await delay(10); await button.click(); + // Allow some time for the transition/animations to occur before assuming the click is done + await delay(10); }; describe('saved objects edition page', () => { From 754d4bc1e0c77556269e231d1a4a8aa5f4e608c8 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 19 Aug 2021 10:10:09 -0400 Subject: [PATCH 27/28] Remove spacesOss dependency from triggers_actions_ui plugin --- x-pack/plugins/triggers_actions_ui/kibana.json | 2 +- .../triggers_actions_ui/public/application/app.tsx | 2 -- .../components/alert_details_route.test.tsx | 10 +++++----- .../alert_details/components/alert_details_route.tsx | 12 ++++++------ .../public/common/lib/kibana/kibana_react.mock.ts | 2 -- x-pack/plugins/triggers_actions_ui/public/plugin.ts | 3 --- x-pack/plugins/triggers_actions_ui/tsconfig.json | 2 +- 7 files changed, 13 insertions(+), 20 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/kibana.json b/x-pack/plugins/triggers_actions_ui/kibana.json index 9e9af347af2a2..4033889d9811e 100644 --- a/x-pack/plugins/triggers_actions_ui/kibana.json +++ b/x-pack/plugins/triggers_actions_ui/kibana.json @@ -8,7 +8,7 @@ "server": true, "ui": true, "optionalPlugins": ["alerting", "features", "home", "spaces"], - "requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects", "spacesOss"], + "requiredPlugins": ["management", "charts", "data", "kibanaReact", "kibanaUtils", "savedObjects"], "configPath": ["xpack", "trigger_actions_ui"], "extraPublicDirs": ["public/common", "public/common/constants"], "requiredBundles": ["home", "alerting", "esUiShared", "kibanaReact", "kibanaUtils"] diff --git a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx index cc34f1beaf6b9..9786f5dcb949d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/app.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/app.tsx @@ -18,7 +18,6 @@ import { ChartsPluginStart } from '../../../../../src/plugins/charts/public'; import { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { PluginStartContract as AlertingStart } from '../../../alerting/public'; import type { SpacesPluginStart } from '../../../spaces/public'; -import type { SpacesOssPluginStart } from '../../../../../src/plugins/spaces_oss/public'; import { suspendedComponentWithProps } from './lib/suspended_component_with_props'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; @@ -37,7 +36,6 @@ export interface TriggersAndActionsUiServices extends CoreStart { charts: ChartsPluginStart; alerting?: AlertingStart; spaces?: SpacesPluginStart; - spacesOss: SpacesOssPluginStart; storage?: Storage; setBreadcrumbs: (crumbs: ChromeBreadcrumb[]) => void; actionTypeRegistry: ActionTypeRegistryContract; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx index 441daad1a50bd..9ecaa3d915551 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.test.tsx @@ -15,7 +15,7 @@ import { ToastsApi } from 'kibana/public'; import { AlertDetailsRoute, getRuleData } from './alert_details_route'; import { Alert } from '../../../../types'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; -import { spacesOssPluginMock } from 'src/plugins/spaces_oss/public/mocks'; +import { spacesPluginMock } from '../../../../../../spaces/public/mocks'; import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); @@ -38,11 +38,11 @@ describe('alert_details_route', () => { jest.clearAllMocks(); }); - const spacesOssMock = spacesOssPluginMock.createStartContract(); + const spacesMock = spacesPluginMock.createStartContract(); async function setup() { const useKibanaMock = useKibana as jest.Mocked; // eslint-disable-next-line react-hooks/rules-of-hooks - useKibanaMock().services.spacesOss = spacesOssMock; + useKibanaMock().services.spaces = spacesMock; } it('render a loader while fetching data', () => { @@ -82,7 +82,7 @@ describe('alert_details_route', () => { expect(loadAlert).toHaveBeenCalledWith(rule.id); expect(resolveRule).toHaveBeenCalledWith(rule.id); - expect((spacesOssMock as any).ui.redirectLegacyUrl).toHaveBeenCalledWith( + expect((spacesMock as any).ui.redirectLegacyUrl).toHaveBeenCalledWith( `insightsAndAlerting/triggersActions/rule/new_id`, `rule` ); @@ -122,7 +122,7 @@ describe('alert_details_route', () => { expect(loadAlert).toHaveBeenCalledWith(rule.id); expect(resolveRule).toHaveBeenCalledWith(rule.id); - expect((spacesOssMock as any).ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ + expect((spacesMock as any).ui.components.getLegacyUrlConflict).toHaveBeenCalledWith({ currentObjectId: 'new_id', objectNoun: 'rule', otherObjectId: rule.id, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx index b6279d7fca100..123d60bb9fea3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details_route.tsx @@ -42,7 +42,7 @@ export const AlertDetailsRoute: React.FunctionComponent const { http, notifications: { toasts }, - spacesOss, + spaces: spacesApi, } = useKibana().services; const { basePath } = http; @@ -68,10 +68,10 @@ export const AlertDetailsRoute: React.FunctionComponent useEffect(() => { if (alert) { const outcome = (alert as ResolvedRule).outcome; - if (outcome === 'aliasMatch' && spacesOss.isSpacesAvailable) { + if (spacesApi && outcome === 'aliasMatch') { // This rule has been resolved from a legacy URL - redirect the user to the new URL and display a toast. const path = basePath.prepend(`insightsAndAlerting/triggersActions/rule/${alert.id}`); - spacesOss.ui.redirectLegacyUrl( + spacesApi.ui.redirectLegacyUrl( path, i18n.translate('xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', { defaultMessage: 'rule', @@ -84,8 +84,8 @@ export const AlertDetailsRoute: React.FunctionComponent const getLegacyUrlConflictCallout = () => { const outcome = (alert as ResolvedRule).outcome; - const aliasTargetId = (alert as ResolvedRule).alias_target_id; - if (outcome === 'conflict' && aliasTargetId && spacesOss.isSpacesAvailable) { + if (spacesApi && outcome === 'conflict') { + const aliasTargetId = (alert as ResolvedRule).alias_target_id!; // This is always defined if outcome === 'conflict' // We have resolved to one rule, but there is another one with a legacy URL associated with this page. Display a // callout with a warning for the user, and provide a way for them to navigate to the other rule. const otherRulePath = basePath.prepend( @@ -94,7 +94,7 @@ export const AlertDetailsRoute: React.FunctionComponent return ( <> - {spacesOss.ui.components.getLegacyUrlConflict({ + {spacesApi.ui.components.getLegacyUrlConflict({ objectNoun: i18n.translate( 'xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', { diff --git a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts index f8aa483711c30..2985a5306ed51 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/lib/kibana/kibana_react.mock.ts @@ -8,7 +8,6 @@ import React from 'react'; import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; -import { spacesOssPluginMock } from '../../../../../../../src/plugins/spaces_oss/public/mocks'; import { coreMock, scopedHistoryMock } from '../../../../../../../src/core/public/mocks'; import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; import { TriggersAndActionsUiServices } from '../../../application/app'; @@ -45,7 +44,6 @@ export const createStartServicesMock = (): TriggersAndActionsUiServices => { element: ({ style: { cursor: 'pointer' }, } as unknown) as HTMLElement, - spacesOss: spacesOssPluginMock.createStartContract(), } as TriggersAndActionsUiServices; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 36d6964ce7753..7661eefba7f65 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -26,7 +26,6 @@ import { PluginStartContract as AlertingStart } from '../../alerting/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; import type { SpacesPluginStart } from '../../spaces/public'; -import type { SpacesOssPluginStart } from '../../../../src/plugins/spaces_oss/public'; import { getAddConnectorFlyoutLazy } from './common/get_add_connector_flyout'; import { getEditConnectorFlyoutLazy } from './common/get_edit_connector_flyout'; @@ -74,7 +73,6 @@ interface PluginsStart { charts: ChartsPluginStart; alerting?: AlertingStart; spaces?: SpacesPluginStart; - spacesOss: SpacesOssPluginStart; navigateToApp: CoreStart['application']['navigateToApp']; features: FeaturesPluginStart; } @@ -150,7 +148,6 @@ export class Plugin charts: pluginsStart.charts, alerting: pluginsStart.alerting, spaces: pluginsStart.spaces, - spacesOss: pluginsStart.spacesOss, element: params.element, storage: new Storage(window.localStorage), setBreadcrumbs: params.setBreadcrumbs, diff --git a/x-pack/plugins/triggers_actions_ui/tsconfig.json b/x-pack/plugins/triggers_actions_ui/tsconfig.json index e3c8b77d2c1d4..ac36780f10c01 100644 --- a/x-pack/plugins/triggers_actions_ui/tsconfig.json +++ b/x-pack/plugins/triggers_actions_ui/tsconfig.json @@ -24,6 +24,6 @@ { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/management/tsconfig.json" }, - { "path": "../../../src/plugins/spaces_oss/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, ] } From 01a9b8686d07e1dc5a2890841697d3d7ef2af48c Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Thu, 19 Aug 2021 10:40:14 -0400 Subject: [PATCH 28/28] Expose useSpaces function to external consumers --- x-pack/plugins/spaces/public/index.ts | 2 +- x-pack/plugins/spaces/public/mocks.ts | 1 + .../spaces/public/spaces_context/context.tsx | 18 +++++++++--------- .../spaces/public/spaces_context/index.ts | 2 +- .../spaces/public/spaces_context/types.ts | 10 ++++------ x-pack/plugins/spaces/public/ui_api/index.ts | 2 ++ x-pack/plugins/spaces/public/ui_api/types.ts | 8 +++++++- 7 files changed, 25 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/spaces/public/index.ts b/x-pack/plugins/spaces/public/index.ts index e7fa0e814b2ca..d13f8f48e6719 100644 --- a/x-pack/plugins/spaces/public/index.ts +++ b/x-pack/plugins/spaces/public/index.ts @@ -30,7 +30,7 @@ export type { SpaceAvatarProps } from './space_avatar'; export type { SpaceListProps } from './space_list'; -export type { SpacesContextProps } from './spaces_context'; +export type { SpacesContextProps, SpacesReactContextValue } from './spaces_context'; export type { LazyComponentFn, SpacesApiUi, SpacesApiUiComponent } from './ui_api'; diff --git a/x-pack/plugins/spaces/public/mocks.ts b/x-pack/plugins/spaces/public/mocks.ts index 31697c86b7f1e..897f58e1d649c 100644 --- a/x-pack/plugins/spaces/public/mocks.ts +++ b/x-pack/plugins/spaces/public/mocks.ts @@ -25,6 +25,7 @@ const createApiUiMock = () => { const mock: SpacesApiUiMock = { components: createApiUiComponentsMock(), redirectLegacyUrl: jest.fn(), + useSpaces: jest.fn(), }; return mock; diff --git a/x-pack/plugins/spaces/public/spaces_context/context.tsx b/x-pack/plugins/spaces/public/spaces_context/context.tsx index 0b051054e7daf..64df17bed5768 100644 --- a/x-pack/plugins/spaces/public/spaces_context/context.tsx +++ b/x-pack/plugins/spaces/public/spaces_context/context.tsx @@ -7,22 +7,22 @@ import * as React from 'react'; +import type { CoreStart } from 'src/core/public'; + import type { SpacesManager } from '../spaces_manager'; import type { SpacesData } from '../types'; -import type { KibanaServices, SpacesReactContext, SpacesReactContextValue } from './types'; +import type { SpacesReactContext, SpacesReactContextValue } from './types'; const { useContext, createElement, createContext } = React; -const context = createContext>>({}); +const context = createContext>>>({}); -export const useSpaces = (): SpacesReactContextValue< - KibanaServices & Extra -> => - useContext( - (context as unknown) as React.Context> - ); +export const useSpaces = < + Services extends Partial +>(): SpacesReactContextValue => + useContext((context as unknown) as React.Context>); -export const createSpacesReactContext = ( +export const createSpacesReactContext = >( services: Services, spacesManager: SpacesManager, spacesDataPromise: Promise diff --git a/x-pack/plugins/spaces/public/spaces_context/index.ts b/x-pack/plugins/spaces/public/spaces_context/index.ts index 710f59d6b16d5..5b5ff829b3800 100644 --- a/x-pack/plugins/spaces/public/spaces_context/index.ts +++ b/x-pack/plugins/spaces/public/spaces_context/index.ts @@ -6,5 +6,5 @@ */ export { useSpaces } from './context'; -export type { SpacesContextProps } from './types'; +export type { SpacesContextProps, SpacesReactContextValue } from './types'; export { getSpacesContextProviderWrapper } from './wrapper'; diff --git a/x-pack/plugins/spaces/public/spaces_context/types.ts b/x-pack/plugins/spaces/public/spaces_context/types.ts index 01057c1550f0c..d3a6859875b9c 100644 --- a/x-pack/plugins/spaces/public/spaces_context/types.ts +++ b/x-pack/plugins/spaces/public/spaces_context/types.ts @@ -13,18 +13,16 @@ import type { PluginsStart } from '../plugin'; import type { SpacesManager } from '../spaces_manager'; import type { SpacesData } from '../types'; -export type KibanaServices = Partial; - -export interface SpacesReactContextValue { +export interface SpacesReactContextValue> { readonly spacesManager: SpacesManager; readonly spacesDataPromise: Promise; readonly services: Services; } -export interface SpacesReactContext { - value: SpacesReactContextValue; +export interface SpacesReactContext> { + value: SpacesReactContextValue; Provider: React.FC; - Consumer: React.Consumer>; + Consumer: React.Consumer>; } export interface InternalProps { diff --git a/x-pack/plugins/spaces/public/ui_api/index.ts b/x-pack/plugins/spaces/public/ui_api/index.ts index 0a88f772a76e8..e0749b04de139 100644 --- a/x-pack/plugins/spaces/public/ui_api/index.ts +++ b/x-pack/plugins/spaces/public/ui_api/index.ts @@ -9,6 +9,7 @@ import type { StartServicesAccessor } from 'src/core/public'; import type { PluginsStart } from '../plugin'; import { createRedirectLegacyUrl } from '../share_saved_objects_to_space'; +import { useSpaces } from '../spaces_context'; import type { SpacesManager } from '../spaces_manager'; import { getComponents } from './components'; import type { LazyComponentFn, SpacesApiUi, SpacesApiUiComponent } from './types'; @@ -26,5 +27,6 @@ export const getUiApi = ({ spacesManager, getStartServices }: GetUiApiOptions): return { components, redirectLegacyUrl: createRedirectLegacyUrl(getStartServices), + useSpaces, }; }; diff --git a/x-pack/plugins/spaces/public/ui_api/types.ts b/x-pack/plugins/spaces/public/ui_api/types.ts index 5371b2b9ae8e2..5048e5a9b9652 100644 --- a/x-pack/plugins/spaces/public/ui_api/types.ts +++ b/x-pack/plugins/spaces/public/ui_api/types.ts @@ -7,6 +7,8 @@ import type { ReactElement } from 'react'; +import type { CoreStart } from 'src/core/public'; + import type { CopyToSpaceFlyoutProps } from '../copy_saved_objects_to_space'; import type { LegacyUrlConflictProps, @@ -14,7 +16,7 @@ import type { } from '../share_saved_objects_to_space'; import type { SpaceAvatarProps } from '../space_avatar'; import type { SpaceListProps } from '../space_list'; -import type { SpacesContextProps } from '../spaces_context'; +import type { SpacesContextProps, SpacesReactContextValue } from '../spaces_context'; /** * Function that returns a promise for a lazy-loadable component. @@ -50,6 +52,10 @@ export interface SpacesApiUi { * location_. Default value is 'object'. */ redirectLegacyUrl: (path: string, objectNoun?: string) => Promise; + /** + * Helper function to easily access the Spaces React Context provider. + */ + useSpaces>(): SpacesReactContextValue; } /**