diff --git a/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml b/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml new file mode 100644 index 0000000000000..3df81d900f8cc --- /dev/null +++ b/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml @@ -0,0 +1,53 @@ +# yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/rre.schema.json +apiVersion: backstage.io/v1alpha1 +kind: Resource +metadata: + name: bk-kibana-chrome-forward-testing + description: 'Chrome Forward Testing for Kibana' + links: + - url: 'https://buildkite.com/elastic/kibana-chrome-forward-testing' + title: Pipeline link +spec: + type: buildkite-pipeline + system: buildkite + owner: 'group:kibana-operations' + implementation: + apiVersion: buildkite.elastic.dev/v1 + kind: Pipeline + metadata: + name: kibana / chrome forward testing + description: 'Testing Kibana against upcoming versions of Chrome' + spec: + env: + # This is what will switch the FTRs pipeline to use Chrome Beta + USE_CHROME_BETA: 'true' + # Unit-tests don't depend on Chrome's versions, integration tests , so we don't need to run those + LIMIT_CONFIG_TYPE: 'functional' + SLACK_NOTIFICATIONS_CHANNEL: '#kibana-operations-alerts' + ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' + + allow_rebuilds: true + branch_configuration: main + default_branch: main + repository: elastic/kibana + pipeline_file: .buildkite/scripts/pipelines/pull_request/pipeline.sh + skip_intermediate_builds: true + provider_settings: + prefix_pull_request_fork_branch_names: false + skip_pull_request_builds_for_existing_commits: true + trigger_mode: none + teams: + kibana-operations: + access_level: MANAGE_BUILD_AND_READ + appex-qa: + access_level: MANAGE_BUILD_AND_READ + kibana-tech-leads: + access_level: MANAGE_BUILD_AND_READ + everyone: + access_level: BUILD_AND_READ + # Scheduled runs for the pipeline + schedules: + Daily 12 pm UTC: + cronline: 0 12 * * * + message: Daily Chrome Forward Testing + branch: main diff --git a/.buildkite/pipeline-resource-definitions/locations.yml b/.buildkite/pipeline-resource-definitions/locations.yml index 5144982a0627d..ab584690ca8d1 100644 --- a/.buildkite/pipeline-resource-definitions/locations.yml +++ b/.buildkite/pipeline-resource-definitions/locations.yml @@ -13,6 +13,8 @@ spec: - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-artifacts-snapshot.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-artifacts-staging.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-artifacts-trigger.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-chrome-forward-testing.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-codeql.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-coverage-daily.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-es-forward-testing.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-es-serverless-snapshots.yml @@ -26,11 +28,12 @@ spec: - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-on-merge.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-performance-daily.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-performance-data-set-extraction-daily.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-pointer-compression.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-pr.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-purge-cloud-deployments.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-emergency-release.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates-emergency.yml + - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-quality-gates.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-release-testing.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-serverless-release.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/scalability_testing-daily.yml @@ -43,5 +46,3 @@ spec: - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-investigations.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/security-solution-quality-gate/kibana-serverless-security-solution-quality-gate-rule-management.yml - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/trigger-version-dependent-jobs.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-pointer-compression.yml - - https://github.com/elastic/kibana/blob/main/.buildkite/pipeline-resource-definitions/kibana-codeql.yml diff --git a/package.json b/package.json index 28b4a344b4cd8..f04b96e75345b 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "**/@bazel/typescript/protobufjs": "6.11.4", "**/@hello-pangea/dnd": "16.6.0", "**/@langchain/core": "^0.2.18", + "**/@langchain/google-common": "^0.1.1", "**/@types/node": "20.10.5", "**/@typescript-eslint/utils": "5.62.0", "**/chokidar": "^3.5.3", @@ -999,11 +1000,13 @@ "@kbn/zod-helpers": "link:packages/kbn-zod-helpers", "@langchain/community": "0.2.18", "@langchain/core": "^0.2.18", - "@langchain/google-genai": "^0.0.23", + "@langchain/google-common": "^0.1.1", + "@langchain/google-genai": "^0.1.0", + "@langchain/google-vertexai": "^0.1.0", "@langchain/langgraph": "0.0.34", "@langchain/openai": "^0.1.3", "@langtrase/trace-attributes": "^3.0.8", - "@launchdarkly/node-server-sdk": "^9.5.4", + "@launchdarkly/node-server-sdk": "^9.6.0", "@launchdarkly/openfeature-node-server": "^1.0.0", "@loaders.gl/core": "^3.4.7", "@loaders.gl/json": "^3.4.7", @@ -1148,7 +1151,7 @@ "jsts": "^1.6.2", "kea": "^2.6.0", "langchain": "^0.2.11", - "langsmith": "^0.1.39", + "langsmith": "^0.1.55", "launchdarkly-js-client-sdk": "^3.4.0", "load-json-file": "^6.2.0", "lodash": "^4.17.21", diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts index 06e5156dfac6e..9a45290c95389 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/utils.ts @@ -274,11 +274,6 @@ function validateNodeProps< `[Chrome navigation] Error in node [${id}]. Only one of "href" or "cloudLink" can be provided.` ); } - if (renderAs === 'panelOpener' && !link) { - throw new Error( - `[Chrome navigation] Error in node [${id}]. If renderAs is set to "panelOpener", a "link" must also be provided.` - ); - } if (renderAs === 'item' && !link && !onClick) { throw new Error( `[Chrome navigation] Error in node [${id}]. If renderAs is set to "item", a "link" or "onClick" must also be provided.` diff --git a/packages/deeplinks/observability/locators/dataset_quality_details.ts b/packages/deeplinks/observability/locators/dataset_quality_details.ts index c53cabcf6dbb2..032d5d65f5665 100644 --- a/packages/deeplinks/observability/locators/dataset_quality_details.ts +++ b/packages/deeplinks/observability/locators/dataset_quality_details.ts @@ -42,4 +42,5 @@ export interface DataQualityDetailsLocatorParams extends SerializableRecord { table?: DegradedFieldsTable; }; expandedDegradedField?: string; + showCurrentQualityIssues?: boolean; } diff --git a/packages/kbn-alerts-ui-shared/src/alert_filter_controls/filter_group.tsx b/packages/kbn-alerts-ui-shared/src/alert_filter_controls/filter_group.tsx index 01a1524c4abc4..b97c0940f41d6 100644 --- a/packages/kbn-alerts-ui-shared/src/alert_filter_controls/filter_group.tsx +++ b/packages/kbn-alerts-ui-shared/src/alert_filter_controls/filter_group.tsx @@ -44,6 +44,7 @@ import { URL_PARAM_ARRAY_EXCEPTION_MSG } from './translations'; export const FilterGroup = (props: PropsWithChildren) => { const { + featureIds, dataViewId, onFiltersChange, timeRange, @@ -58,7 +59,7 @@ export const FilterGroup = (props: PropsWithChildren) => { maxControls = Infinity, ControlGroupRenderer, Storage, - featureIds, + storageKey, } = props; const filterChangedSubscription = useRef(); @@ -79,8 +80,8 @@ export const FilterGroup = (props: PropsWithChildren) => { const [controlGroup, setControlGroup] = useState(); const localStoragePageFilterKey = useMemo( - () => `${featureIds.join(',')}.${spaceId}.${URL_PARAM_KEY}`, - [featureIds, spaceId] + () => storageKey ?? `${featureIds.join(',')}.${spaceId}.${URL_PARAM_KEY}`, + [featureIds, spaceId, storageKey] ); const currentFiltersRef = useRef(); diff --git a/packages/kbn-alerts-ui-shared/src/alert_filter_controls/types.ts b/packages/kbn-alerts-ui-shared/src/alert_filter_controls/types.ts index 5fb7bdd1a6a0f..19a76c76ff6f8 100644 --- a/packages/kbn-alerts-ui-shared/src/alert_filter_controls/types.ts +++ b/packages/kbn-alerts-ui-shared/src/alert_filter_controls/types.ts @@ -74,4 +74,5 @@ export interface FilterGroupProps extends Pick = ({ hasBorder title={rule.name} onClick={() => onSelectRuleType(rule.id)} - description={ - <> - {rule.description} - {rule.description && } - - {producerToDisplayName(rule.producer)} - - - } + description={rule.description} style={{ marginRight: '8px', flexGrow: 0 }} data-test-subj={`${rule.id}-SelectOption`} isDisabled={rule.enabledInLicense === false} - /> + > + + {producerToDisplayName(rule.producer)} + + ))} diff --git a/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_modal.tsx b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_modal.tsx index a459d4cb76311..f2d95f44516ab 100644 --- a/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_modal.tsx +++ b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_modal.tsx @@ -121,7 +121,7 @@ export const RuleTypeModal: React.FC = {ruleTypesLoading ? ( diff --git a/packages/kbn-eslint-plugin-eslint/index.js b/packages/kbn-eslint-plugin-eslint/index.js index 5ff3d70ae8a32..8c52f916ec206 100644 --- a/packages/kbn-eslint-plugin-eslint/index.js +++ b/packages/kbn-eslint-plugin-eslint/index.js @@ -20,5 +20,6 @@ module.exports = { no_this_in_property_initializers: require('./rules/no_this_in_property_initializers'), no_unsafe_console: require('./rules/no_unsafe_console'), no_unsafe_hash: require('./rules/no_unsafe_hash'), + no_deprecated_authz_config: require('./rules/no_deprecated_authz_config'), }, }; diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_authz_config.js b/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_authz_config.js index ca2821c4f8ce6..f6485d0914c15 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_authz_config.js +++ b/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_authz_config.js @@ -12,15 +12,16 @@ const ACCESS_TAG_PREFIX = 'access:'; const isStringLiteral = (el) => el.type === 'Literal' && typeof el.value === 'string'; const isLiteralAccessTag = (el) => isStringLiteral(el) && el.value.startsWith(ACCESS_TAG_PREFIX); -const isLiteralNonAccessTag = (el) => - isStringLiteral(el) && !el.value.startsWith(ACCESS_TAG_PREFIX); const isTemplateLiteralAccessTag = (el) => el.type === 'TemplateLiteral' && el.quasis[0].value.raw.startsWith(ACCESS_TAG_PREFIX); -const isTemplateLiteralNonAccessTag = (el) => - el.type === 'TemplateLiteral' && !el.quasis[0].value.raw.startsWith(ACCESS_TAG_PREFIX); const maybeReportDisabledSecurityConfig = (node, context, isVersionedRoute = false) => { + // Allow disabling migration for routes that are opted out from authorization + if (process.env.MIGRATE_DISABLED_AUTHZ === 'false') { + return; + } + const callee = node.callee; const isAddVersionCall = callee.type === 'MemberExpression' && callee.property.name === 'addVersion'; @@ -166,11 +167,11 @@ const handleRouteConfig = (node, context, isVersionedRoute = false) => { const accessTagsFilter = (el) => isLiteralAccessTag(el) || isTemplateLiteralAccessTag(el); const nonAccessTagsFilter = (el) => - isLiteralNonAccessTag(el) || isTemplateLiteralNonAccessTag(el); + !isLiteralAccessTag(el) && !isTemplateLiteralAccessTag(el); const getAccessPrivilege = (el) => { if (el.type === 'Literal') { - return `'${el.value.split(':')[1]}'`; + return `'${el.value.split(ACCESS_TAG_PREFIX)[1]}'`; } if (el.type === 'TemplateLiteral') { diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_authz_config.test.js b/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_authz_config.test.js index f0b64da01cf75..b397c4457b2c7 100644 --- a/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_authz_config.test.js +++ b/packages/kbn-eslint-plugin-eslint/rules/no_deprecated_authz_config.test.js @@ -158,6 +158,28 @@ ruleTester.run('no_deprecated_authz_config', rule, { `, name: 'invalid: access tags are string literals, move to security.authz.requiredPrivileges', }, + { + code: ` + router.get({ + path: '/some/path', + options: { + tags: ['access:ml:someTag', 'access:prefix:someTag'], + }, + }); + `, + errors: [{ message: "Move 'access' tags to security.authz.requiredPrivileges." }], + output: ` + router.get({ + path: '/some/path', + security: { + authz: { + requiredPrivileges: ['ml:someTag', 'prefix:someTag'], + }, + }, + }); + `, + name: 'invalid: access tags have multiple prefixes, move to security.authz.requiredPrivileges', + }, { code: ` router.get({ @@ -180,6 +202,30 @@ ruleTester.run('no_deprecated_authz_config', rule, { `, name: 'invalid: access tags are template literals, move to security.authz.requiredPrivileges', }, + { + code: ` + router.get({ + path: '/some/path', + options: { + tags: ['access:securitySolution', routeTagHelper('someTag')], + }, + }); + `, + errors: [{ message: "Move 'access' tags to security.authz.requiredPrivileges." }], + output: ` + router.get({ + path: '/some/path', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + },options: { + tags: [routeTagHelper('someTag')], + }, + }); + `, + name: 'invalid: access tags and tags made with helper function, only access tags are moved to security.authz.requiredPrivileges', + }, { code: ` router.get({ diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 8af1cb4460ecb..bd0234eaa87ec 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -27,7 +27,7 @@ pageLoadAssetSize: dashboardEnhanced: 65646 data: 454087 dataQuality: 19384 - datasetQuality: 52000 + datasetQuality: 55000 dataUsage: 30000 dataViewEditor: 28082 dataViewFieldEditor: 42021 diff --git a/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx b/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx index 9f97a8ee63769..3d8ea94b3599a 100644 --- a/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx +++ b/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx @@ -42,8 +42,8 @@ export const SchedulePanel: FC> = ({ <> - -

{title}

+ +

{title}

@@ -115,7 +115,6 @@ export const ConnectorSchedulingComponent: React.FC - {hasIngestionError ? : <>} {children} diff --git a/packages/kbn-search-connectors/components/scheduling/full_content.tsx b/packages/kbn-search-connectors/components/scheduling/full_content.tsx index ced6c9ef8442e..de85f8fb2e4a9 100644 --- a/packages/kbn-search-connectors/components/scheduling/full_content.tsx +++ b/packages/kbn-search-connectors/components/scheduling/full_content.tsx @@ -129,10 +129,10 @@ export const ConnectorContentScheduling: React.FC + - -

{getAccordionTitle(type)}

+ +
{getAccordionTitle(type)}
diff --git a/packages/kbn-search-index-documents/components/documents_overview.test.tsx b/packages/kbn-search-index-documents/components/documents_overview.test.tsx index c11cc5eb2d98d..3f452e2e072a4 100644 --- a/packages/kbn-search-index-documents/components/documents_overview.test.tsx +++ b/packages/kbn-search-index-documents/components/documents_overview.test.tsx @@ -27,6 +27,6 @@ describe('DocumentList', () => { ); - expect(screen.getByText('Browse documents')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Search documents in this index')).toBeInTheDocument(); }); }); diff --git a/packages/kbn-search-index-documents/components/documents_overview.tsx b/packages/kbn-search-index-documents/components/documents_overview.tsx index d5f6807e313a7..8b5f210e8dd2e 100644 --- a/packages/kbn-search-index-documents/components/documents_overview.tsx +++ b/packages/kbn-search-index-documents/components/documents_overview.tsx @@ -7,16 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { - EuiFieldSearch, - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiSpacer, - EuiTitle, -} from '@elastic/eui'; +import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiPanel, useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { ChangeEvent } from 'react'; +import { css } from '@emotion/react'; interface DocumentsProps { accessControlSwitch?: React.ReactNode; @@ -30,22 +24,22 @@ export const DocumentsOverview: React.FC = ({ documentComponent, searchQueryCallback, }) => { + const { euiTheme } = useEuiTheme(); return ( - - - - -

- {i18n.translate('searchIndexDocuments.documents.title', { - defaultMessage: 'Browse documents', - })} -

-
-
- {accessControlSwitch && {accessControlSwitch}} + + {accessControlSwitch && ( + + {accessControlSwitch} + + )} = ({ item, navigateToUrl, active const isNotMobile = useIsWithinMinBreakpoint('s'); const isIconVisible = isNotMobile && !isSideNavCollapsed && !!children && children.length > 0; const isActive = isActiveFromUrl(item.path, activeNodes); + const hasLandingPage = Boolean(href); const itemClassNames = classNames( 'sideNavItem', @@ -73,30 +74,36 @@ export const NavigationItemOpenPanel: FC = ({ item, navigateToUrl, active [`nav-item-id-${id}`]: id, [`nav-item-isActive`]: isActive, }); + const buttonDataTestSubj = classNames(`panelOpener`, `panelOpener-${path}`, { [`panelOpener-id-${id}`]: id, [`panelOpener-deepLinkId-${deepLink?.id}`]: !!deepLink, }); + const togglePanel = useCallback(() => { + if (selectedNode?.id === item.id) { + closePanel(); + } else { + openPanel(item); + } + }, [selectedNode?.id, item, closePanel, openPanel]); + const onLinkClick = useCallback( (e: React.MouseEvent) => { if (!href) { + togglePanel(); return; } e.preventDefault(); navigateToUrl(href); closePanel(); }, - [closePanel, href, navigateToUrl] + [closePanel, href, navigateToUrl, togglePanel] ); const onIconClick = useCallback(() => { - if (selectedNode?.id === item.id) { - closePanel(); - } else { - openPanel(item); - } - }, [openPanel, closePanel, item, selectedNode]); + togglePanel(); + }, [togglePanel]); const isExpanded = selectedNode?.path === path; @@ -123,7 +130,7 @@ export const NavigationItemOpenPanel: FC = ({ item, navigateToUrl, active size="s" color="text" onClick={onIconClick} - iconType="spaces" + iconType={hasLandingPage ? 'spaces' : 'arrowRight'} iconSize="m" aria-label={i18n.translate('sharedUXPackages.chrome.sideNavigation.togglePanel', { defaultMessage: 'Toggle "{title}" panel navigation', diff --git a/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx b/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx index 5077cefc44625..f991dd6a30714 100644 --- a/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx +++ b/packages/shared-ux/chrome/navigation/src/ui/components/panel/navigation_panel.tsx @@ -32,6 +32,12 @@ const getTestSubj = (selectedNode: PanelSelectedNode | null): string | undefined }); }; +const getTargetTestSubj = (target: EventTarget | null): string | undefined => { + if (!target) return; + + return (target as HTMLElement).dataset.testSubj; +}; + export const NavigationPanel: FC = () => { const { euiTheme } = useEuiTheme(); const { isOpen, close, getContent, selectedNode } = usePanel(); @@ -48,12 +54,22 @@ export const NavigationPanel: FC = () => { const onOutsideClick = useCallback( ({ target }: Event) => { - // Only close if we are not clicking on the currently selected nav node - if ( - !(target as HTMLButtonElement).dataset.testSubj?.includes( - `panelOpener-${selectedNode?.path}` - ) - ) { + let doClose = true; + + if (target) { + // Only close if we are not clicking on the currently selected nav node + const testSubj = + getTargetTestSubj(target) ?? getTargetTestSubj((target as HTMLElement).parentNode); + + if ( + testSubj?.includes(`nav-item-${selectedNode?.path}`) || + testSubj?.includes(`panelOpener-${selectedNode?.path}`) + ) { + doClose = false; + } + } + + if (doClose) { close(); } }, diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.scss b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.scss index 0f463926908f3..4b1a1d9b27d57 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.scss +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/editor_menu.scss @@ -1,6 +1,6 @@ .dshSolutionToolbar__editorContextMenu { - @include euiScrollBar; - @include euiOverflowShadow; max-height: 60vh; overflow-y: scroll; + @include euiScrollBar; + @include euiOverflowShadow; } diff --git a/src/plugins/expression_error/public/components/debug/debug.scss b/src/plugins/expression_error/public/components/debug/debug.scss index e4d1422975675..fcc046cae7e96 100644 --- a/src/plugins/expression_error/public/components/debug/debug.scss +++ b/src/plugins/expression_error/public/components/debug/debug.scss @@ -4,10 +4,10 @@ height: 100%; .canvasDebug__content { - @include euiScrollBar; width: 100%; height: 100%; overflow: auto; padding: $euiSize; + @include euiScrollBar; } } diff --git a/src/plugins/input_control_vis/public/components/vis/input_control_vis.scss b/src/plugins/input_control_vis/public/components/vis/input_control_vis.scss index 322573446f762..42ec14a70b02e 100644 --- a/src/plugins/input_control_vis/public/components/vis/input_control_vis.scss +++ b/src/plugins/input_control_vis/public/components/vis/input_control_vis.scss @@ -1,9 +1,9 @@ .icvContainer__wrapper { - @include euiScrollBar; min-height: 0; flex: 1 1 0; display: flex; overflow: auto; + @include euiScrollBar; } .icvContainer { diff --git a/test/functional/page_objects/solution_navigation.ts b/test/functional/page_objects/solution_navigation.ts index 882c5a478dbce..a0544e1100507 100644 --- a/test/functional/page_objects/solution_navigation.ts +++ b/test/functional/page_objects/solution_navigation.ts @@ -59,7 +59,11 @@ export function SolutionNavigationProvider(ctx: Pick { + const passThrough = new PassThrough(); + + // Write the data chunks to the stream + setTimeout(() => { + passThrough.write( + Buffer.from( + `data: {"candidates": [{"content": {"role": "model","parts": [{"text": "token1"}]}}],"modelVersion": "gemini-1.5-pro-001"}` + ) + ); + }); + setTimeout(() => { + passThrough.write( + Buffer.from( + `data: {"candidates": [{"content": {"role": "model","parts": [{"text": "token2"}]}}],"modelVersion": "gemini-1.5-pro-001"}` + ) + ); + }); + setTimeout(() => { + passThrough.write( + Buffer.from( + `data: {"candidates": [{"content": {"role": "model","parts": [{"text": "token3"}]}}],"modelVersion": "gemini-1.5-pro-001"}` + ) + ); + // End the stream + passThrough.end(); + }); + + return { + data: passThrough, // PassThrough stream will act as the async iterator + status: 'ok', + }; +}); + +const callMessages = [ + new SystemMessage('Answer the following questions truthfully and as best you can.'), + new HumanMessage('Question: Do you know my name?\n\n'), +] as unknown as BaseMessage[]; + +const callOptions = { + stop: ['\n'], + recursionLimit: 0, + /** Maximum number of parallel calls to make. */ + maxConcurrency: 0, +}; +const handleLLMNewToken = jest.fn(); +const callRunManager = { + handleLLMNewToken, +} as unknown as CallbackManagerForLLMRun; +const onFailedAttempt = jest.fn(); +const defaultArgs = { + actionsClient, + connectorId, + logger: mockLogger, + streaming: false, + maxRetries: 0, + onFailedAttempt, +}; + +const testMessage = 'Yes, your name is Andrew. How can I assist you further, Andrew?'; + +export const mockActionResponse = { + candidates: [ + { + content: { + role: 'model', + parts: [ + { + text: testMessage, + }, + ], + }, + finishReason: 'STOP', + }, + ], + usageMetadata: { input_tokens: 4, output_tokens: 10, total_tokens: 14 }, +}; + +describe('ActionsClientChatVertexAI', () => { + beforeEach(() => { + jest.clearAllMocks(); + actionsClient.execute.mockImplementation( + jest.fn().mockImplementation(() => ({ + data: mockActionResponse, + status: 'ok', + })) + ); + mockExecute.mockImplementation(() => ({ + data: mockActionResponse, + status: 'ok', + })); + }); + + describe('_generate streaming: false', () => { + it('returns the expected content when _generate is invoked', async () => { + const actionsClientChatVertexAI = new ActionsClientChatVertexAI(defaultArgs); + + const result = await actionsClientChatVertexAI._generate( + callMessages, + callOptions, + callRunManager + ); + const subAction = actionsClient.execute.mock.calls[0][0].params.subAction; + expect(subAction).toEqual('invokeAIRaw'); + + expect(result.generations[0].text).toEqual(testMessage); + }); + + it('rejects with the expected error when the action result status is error', async () => { + const hasErrorStatus = jest.fn().mockImplementation(() => { + throw new Error( + 'ActionsClientChatVertexAI: action result status is error: action-result-message - action-result-service-message' + ); + }); + + actionsClient.execute.mockRejectedValueOnce(hasErrorStatus); + + const actionsClientChatVertexAI = new ActionsClientChatVertexAI({ + ...defaultArgs, + actionsClient, + }); + + await expect( + actionsClientChatVertexAI._generate(callMessages, callOptions, callRunManager) + ).rejects.toThrowError(); + expect(onFailedAttempt).toHaveBeenCalled(); + }); + + it('rejects with the expected error the message has invalid content', async () => { + actionsClient.execute.mockImplementation( + jest.fn().mockResolvedValue({ + data: { + Bad: true, + finishReason: 'badness', + }, + status: 'ok', + }) + ); + + const actionsClientChatVertexAI = new ActionsClientChatVertexAI(defaultArgs); + + await expect( + actionsClientChatVertexAI._generate(callMessages, callOptions, callRunManager) + ).rejects.toThrowError("Cannot read properties of undefined (reading 'text')"); + }); + }); + + describe('*_streamResponseChunks', () => { + it('iterates over gemini chunks', async () => { + actionsClient.execute.mockImplementationOnce(mockStreamExecute); + + const actionsClientChatVertexAI = new ActionsClientChatVertexAI({ + ...defaultArgs, + actionsClient, + streaming: true, + }); + + const gen = actionsClientChatVertexAI._streamResponseChunks( + callMessages, + callOptions, + callRunManager + ); + + const chunks = []; + + for await (const chunk of gen) { + chunks.push(chunk); + } + + expect(chunks.map((c) => c.text)).toEqual(['token1', 'token2', 'token3']); + expect(handleLLMNewToken).toHaveBeenCalledTimes(3); + expect(handleLLMNewToken).toHaveBeenCalledWith('token1'); + expect(handleLLMNewToken).toHaveBeenCalledWith('token2'); + expect(handleLLMNewToken).toHaveBeenCalledWith('token3'); + }); + }); +}); diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.ts new file mode 100644 index 0000000000000..5627abe717291 --- /dev/null +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/chat_vertex.ts @@ -0,0 +1,187 @@ +/* + * 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 { EnhancedGenerateContentResponse } from '@google/generative-ai'; +import { ActionsClient } from '@kbn/actions-plugin/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { BaseMessage, UsageMetadata } from '@langchain/core/messages'; +import { ChatGenerationChunk } from '@langchain/core/outputs'; +import { ChatVertexAI } from '@langchain/google-vertexai'; +import { get } from 'lodash/fp'; +import { Readable } from 'stream'; + +import { Logger } from '@kbn/logging'; +import { BaseChatModelParams } from '@langchain/core/language_models/chat_models'; +import { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager'; +import { GeminiPartText } from '@langchain/google-common/dist/types'; +import { + convertResponseBadFinishReasonToErrorMsg, + convertResponseContentToChatGenerationChunk, +} from '../../utils/gemini'; +import { ActionsClientChatConnection } from './connection'; + +const DEFAULT_GEMINI_TEMPERATURE = 0; +export interface CustomChatModelInput extends BaseChatModelParams { + actionsClient: PublicMethodsOf; + connectorId: string; + logger: Logger; + streaming: boolean; + temperature?: number; + signal?: AbortSignal; + model?: string; + maxTokens?: number; +} + +export class ActionsClientChatVertexAI extends ChatVertexAI { + #actionsClient: PublicMethodsOf; + #connectorId: string; + #model?: string; + constructor({ actionsClient, connectorId, ...props }: CustomChatModelInput) { + super({ + ...props, + maxOutputTokens: props.maxTokens ?? 2048, + temperature: props.temperature ?? DEFAULT_GEMINI_TEMPERATURE, + }); + // LangChain needs model to be defined for logging purposes + this.model = props.model ?? this.model; + // If model is not specified by consumer, the connector will define it so do not pass + // a LangChain default to the actionsClient + this.#model = props.model; + this.#actionsClient = actionsClient; + this.#connectorId = connectorId; + const client = this.buildClient(props); + this.connection = new ActionsClientChatConnection( + { + ...this, + }, + this.caller, + client, + false, + actionsClient, + connectorId + ); + } + + buildConnection() { + // prevent ChatVertexAI from overwriting our this.connection defined in super + } + + async *_streamResponseChunks( + messages: BaseMessage[], + options: this['ParsedCallOptions'], + runManager?: CallbackManagerForLLMRun + ): AsyncGenerator { + const parameters = this.invocationParams(options); + const data = await this.connection.formatData(messages, parameters); + const stream = await this.caller.callWithOptions({ signal: options?.signal }, async () => { + const systemPart: GeminiPartText | undefined = data?.systemInstruction + ?.parts?.[0] as unknown as GeminiPartText; + const systemInstruction = systemPart?.text.length + ? { systemInstruction: systemPart?.text } + : {}; + const requestBody = { + actionId: this.#connectorId, + params: { + subAction: 'invokeStream', + subActionParams: { + model: this.#model, + messages: data?.contents, + tools: data?.tools, + temperature: this.temperature, + ...systemInstruction, + }, + }, + }; + + const actionResult = await this.#actionsClient.execute(requestBody); + + if (actionResult.status === 'error') { + throw new Error( + `ActionsClientChatVertexAI: action result status is error: ${actionResult?.message} - ${actionResult?.serviceMessage}` + ); + } + + const readable = get('data', actionResult) as Readable; + + if (typeof readable?.read !== 'function') { + throw new Error('Action result status is error: result is not streamable'); + } + return readable; + }); + let usageMetadata: UsageMetadata | undefined; + let index = 0; + let partialStreamChunk = ''; + for await (const rawStreamChunk of stream) { + const streamChunk = rawStreamChunk.toString(); + const nextChunk = `${partialStreamChunk + streamChunk}`; + + let parsedStreamChunk: EnhancedGenerateContentResponse | null = null; + try { + parsedStreamChunk = JSON.parse(nextChunk.replaceAll('data: ', '').replaceAll('\r\n', '')); + partialStreamChunk = ''; + } catch (_) { + partialStreamChunk += nextChunk; + } + + if (parsedStreamChunk !== null && !parsedStreamChunk.candidates?.[0]?.finishReason) { + const response = { + ...parsedStreamChunk, + functionCalls: () => + parsedStreamChunk?.candidates?.[0]?.content.parts[0].functionCall + ? [parsedStreamChunk.candidates?.[0]?.content.parts[0].functionCall] + : [], + }; + + if ( + 'usageMetadata' in response && + this.streamUsage !== false && + options.streamUsage !== false + ) { + const genAIUsageMetadata = response.usageMetadata as { + promptTokenCount: number; + candidatesTokenCount: number; + totalTokenCount: number; + }; + if (!usageMetadata) { + usageMetadata = { + input_tokens: genAIUsageMetadata.promptTokenCount, + output_tokens: genAIUsageMetadata.candidatesTokenCount, + total_tokens: genAIUsageMetadata.totalTokenCount, + }; + } else { + // Under the hood, LangChain combines the prompt tokens. Google returns the updated + // total each time, so we need to find the difference between the tokens. + const outputTokenDiff = + genAIUsageMetadata.candidatesTokenCount - usageMetadata.output_tokens; + usageMetadata = { + input_tokens: 0, + output_tokens: outputTokenDiff, + total_tokens: outputTokenDiff, + }; + } + } + + const chunk = convertResponseContentToChatGenerationChunk(response, { + usageMetadata, + index, + }); + index += 1; + + if (chunk) { + yield chunk; + await runManager?.handleLLMNewToken(chunk.text ?? ''); + } + } else if (parsedStreamChunk) { + // handle bad finish reason + const errorMessage = convertResponseBadFinishReasonToErrorMsg(parsedStreamChunk); + if (errorMessage != null) { + throw new Error(errorMessage); + } + } + } + } +} diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts new file mode 100644 index 0000000000000..0340d71b438db --- /dev/null +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/connection.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ChatConnection, + GoogleAbstractedClient, + GoogleAIBaseLLMInput, + GoogleLLMResponse, +} from '@langchain/google-common'; +import { ActionsClient } from '@kbn/actions-plugin/server'; +import { PublicMethodsOf } from '@kbn/utility-types'; +import { EnhancedGenerateContentResponse } from '@google/generative-ai'; +import { AsyncCaller } from '@langchain/core/utils/async_caller'; +import { convertResponseBadFinishReasonToErrorMsg } from '../../utils/gemini'; + +// only implements non-streaming requests +// stream is handled by ActionsClientChatVertexAI.*_streamResponseChunks +export class ActionsClientChatConnection extends ChatConnection { + actionsClient: PublicMethodsOf; + connectorId: string; + #model?: string; + temperature: number; + caller: AsyncCaller; + constructor( + fields: GoogleAIBaseLLMInput, + caller: AsyncCaller, + client: GoogleAbstractedClient, + _streaming: boolean, // defaulting to false in the super + actionsClient: PublicMethodsOf, + connectorId: string + ) { + super(fields, caller, client, false); + this.actionsClient = actionsClient; + this.connectorId = connectorId; + this.caller = caller; + this.#model = fields.model; + this.temperature = fields.temperature ?? 0; + } + + async _request( + // TODO better types here + data: { + contents: unknown; + tools: unknown[]; + systemInstruction?: { parts: [{ text: string }] }; + }, + options: { signal?: AbortSignal } + ) { + const systemInstruction = data?.systemInstruction?.parts?.[0]?.text.length + ? { systemInstruction: data?.systemInstruction?.parts?.[0]?.text } + : {}; + return this.caller.callWithOptions({ signal: options?.signal }, async () => { + try { + const requestBody = { + actionId: this.connectorId, + params: { + subAction: 'invokeAIRaw', + subActionParams: { + model: this.#model, + messages: data?.contents, + tools: data?.tools, + temperature: this.temperature, + ...systemInstruction, + }, + }, + }; + const actionResult = (await this.actionsClient.execute(requestBody)) as { + status: string; + data: EnhancedGenerateContentResponse; + message?: string; + serviceMessage?: string; + }; + + if (actionResult.status === 'error') { + throw new Error( + `ActionsClientChatVertexAI: action result status is error: ${actionResult?.message} - ${actionResult?.serviceMessage}` + ); + } + + if (actionResult.data.candidates && actionResult.data.candidates.length > 0) { + // handle bad finish reason + const errorMessage = convertResponseBadFinishReasonToErrorMsg(actionResult.data); + if (errorMessage != null) { + throw new Error(errorMessage); + } + } + return actionResult as unknown as GoogleLLMResponse; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + // TODO: Improve error handling + if (e.message?.includes('400 Bad Request')) { + e.status = 400; + } + throw e; + } + }); + } +} diff --git a/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/index.ts b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/index.ts new file mode 100644 index 0000000000000..54197f7e2cb08 --- /dev/null +++ b/x-pack/packages/kbn-langchain/server/language_models/chat_vertex/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './chat_vertex'; diff --git a/x-pack/packages/kbn-langchain/server/language_models/gemini_chat.ts b/x-pack/packages/kbn-langchain/server/language_models/gemini_chat.ts index fb98232359340..197360c2f06e6 100644 --- a/x-pack/packages/kbn-langchain/server/language_models/gemini_chat.ts +++ b/x-pack/packages/kbn-langchain/server/language_models/gemini_chat.ts @@ -8,34 +8,24 @@ import { Content, EnhancedGenerateContentResponse, - FunctionCallPart, - FunctionResponsePart, GenerateContentRequest, GenerateContentResult, - InlineDataPart, - POSSIBLE_ROLES, - Part, - TextPart, - FinishReason, - SafetyRating, } from '@google/generative-ai'; import { ActionsClient } from '@kbn/actions-plugin/server'; import { PublicMethodsOf } from '@kbn/utility-types'; import { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager'; -import { ToolCallChunk } from '@langchain/core/dist/messages/tool'; -import { - AIMessageChunk, - BaseMessage, - ChatMessage, - isBaseMessage, - UsageMetadata, -} from '@langchain/core/messages'; +import { BaseMessage, UsageMetadata } from '@langchain/core/messages'; import { ChatGenerationChunk } from '@langchain/core/outputs'; import { ChatGoogleGenerativeAI } from '@langchain/google-genai'; import { Logger } from '@kbn/logging'; import { BaseChatModelParams } from '@langchain/core/language_models/chat_models'; import { get } from 'lodash/fp'; import { Readable } from 'stream'; +import { + convertBaseMessagesToContent, + convertResponseBadFinishReasonToErrorMsg, + convertResponseContentToChatGenerationChunk, +} from '../utils/gemini'; const DEFAULT_GEMINI_TEMPERATURE = 0; export interface CustomChatModelInput extends BaseChatModelParams { @@ -48,12 +38,6 @@ export interface CustomChatModelInput extends BaseChatModelParams { maxTokens?: number; } -// not sure why these properties are not on the type, as they are on the data -interface SafetyReason extends SafetyRating { - blocked: boolean; - severity: string; -} - export class ActionsClientGeminiChatModel extends ChatGoogleGenerativeAI { #actionsClient: PublicMethodsOf; #connectorId: string; @@ -265,257 +249,3 @@ export class ActionsClientGeminiChatModel extends ChatGoogleGenerativeAI { } } } - -export function convertResponseContentToChatGenerationChunk( - response: EnhancedGenerateContentResponse, - extra: { - usageMetadata?: UsageMetadata | undefined; - index: number; - } -): ChatGenerationChunk | null { - if (!response.candidates || response.candidates.length === 0) { - return null; - } - const functionCalls = response.functionCalls(); - const [candidate] = response.candidates; - const { content, ...generationInfo } = candidate; - const text = content?.parts[0]?.text ?? ''; - - const toolCallChunks: ToolCallChunk[] = []; - if (functionCalls) { - toolCallChunks.push( - ...functionCalls.map((fc) => ({ - ...fc, - args: JSON.stringify(fc.args), - index: extra.index, - type: 'tool_call_chunk' as const, - })) - ); - } - return new ChatGenerationChunk({ - text, - message: new AIMessageChunk({ - content: text, - name: !content ? undefined : content.role, - tool_call_chunks: toolCallChunks, - // Each chunk can have unique "generationInfo", and merging strategy is unclear, - // so leave blank for now. - additional_kwargs: {}, - usage_metadata: extra.usageMetadata, - }), - generationInfo, - }); -} - -export function convertAuthorToRole(author: string): (typeof POSSIBLE_ROLES)[number] { - switch (author) { - /** - * Note: Gemini currently is not supporting system messages - * we will convert them to human messages and merge with following - * */ - case 'ai': - case 'model': // getMessageAuthor returns message.name. code ex.: return message.name ?? type; - return 'model'; - case 'system': - case 'human': - return 'user'; - case 'tool': - case 'function': - return 'function'; - default: - throw new Error(`Unknown / unsupported author: ${author}`); - } -} -export function convertBaseMessagesToContent(messages: BaseMessage[], isMultimodalModel: boolean) { - return messages.reduce<{ - content: Content[]; - mergeWithPreviousContent: boolean; - }>( - (acc, message, index) => { - if (!isBaseMessage(message)) { - throw new Error('Unsupported message input'); - } - const author = getMessageAuthor(message); - if (author === 'system' && index !== 0) { - throw new Error('System message should be the first one'); - } - const role = convertAuthorToRole(author); - const parts = convertMessageContentToParts(message, isMultimodalModel); - - if (acc.mergeWithPreviousContent) { - const prevContent = acc.content[acc.content.length - 1]; - if (!prevContent) { - throw new Error( - 'There was a problem parsing your system message. Please try a prompt without one.' - ); - } - prevContent.parts.push(...parts); - - return { - mergeWithPreviousContent: false, - content: acc.content, - }; - } - let actualRole = role; - if (actualRole === 'function') { - // GenerativeAI API will throw an error if the role is not "user" or "model." - actualRole = 'user'; - } - const content: Content = { - role: actualRole, - parts, - }; - return { - mergeWithPreviousContent: author === 'system', - content: [...acc.content, content], - }; - }, - { content: [], mergeWithPreviousContent: false } - ).content; -} - -export function convertMessageContentToParts( - message: BaseMessage, - isMultimodalModel: boolean -): Part[] { - if (typeof message.content === 'string' && message.content !== '') { - return [{ text: message.content }]; - } - - let functionCalls: FunctionCallPart[] = []; - let functionResponses: FunctionResponsePart[] = []; - let messageParts: Part[] = []; - - if ( - 'tool_calls' in message && - Array.isArray(message.tool_calls) && - message.tool_calls.length > 0 - ) { - functionCalls = message.tool_calls.map((tc) => ({ - functionCall: { - name: tc.name, - args: tc.args, - }, - })); - } else if (message._getType() === 'tool' && message.name && message.content) { - functionResponses = [ - { - functionResponse: { - name: message.name, - response: message.content, - }, - }, - ]; - } else if (Array.isArray(message.content)) { - messageParts = message.content.map((c) => { - if (c.type === 'text') { - return { - text: c.text, - } as TextPart; - } - - if (c.type === 'image_url') { - if (!isMultimodalModel) { - throw new Error(`This model does not support images`); - } - let source; - if (typeof c.image_url === 'string') { - source = c.image_url; - } else if (typeof c.image_url === 'object' && 'url' in c.image_url) { - source = c.image_url.url; - } else { - throw new Error('Please provide image as base64 encoded data URL'); - } - const [dm, data] = source.split(','); - if (!dm.startsWith('data:')) { - throw new Error('Please provide image as base64 encoded data URL'); - } - - const [mimeType, encoding] = dm.replace(/^data:/, '').split(';'); - if (encoding !== 'base64') { - throw new Error('Please provide image as base64 encoded data URL'); - } - - return { - inlineData: { - data, - mimeType, - }, - } as InlineDataPart; - } else if (c.type === 'media') { - return messageContentMedia(c); - } else if (c.type === 'tool_use') { - return { - functionCall: { - name: c.name, - args: c.input, - }, - } as FunctionCallPart; - } - throw new Error(`Unknown content type ${(c as { type: string }).type}`); - }); - } - - return [...messageParts, ...functionCalls, ...functionResponses]; -} - -export function getMessageAuthor(message: BaseMessage) { - const type = message._getType(); - if (ChatMessage.isInstance(message)) { - return message.role; - } - if (type === 'tool') { - return type; - } - return message.name ?? type; -} - -// will be removed once FileDataPart is supported in @langchain/google-genai -function messageContentMedia(content: Record): InlineDataPart { - if ('mimeType' in content && 'data' in content) { - return { - inlineData: { - mimeType: content.mimeType, - data: content.data, - }, - } as InlineDataPart; - } - throw new Error('Invalid media content'); -} - -const badFinishReasons = [FinishReason.RECITATION, FinishReason.SAFETY]; -function hadBadFinishReason(candidate: { finishReason?: FinishReason }) { - return !!candidate.finishReason && badFinishReasons.includes(candidate.finishReason); -} - -export function convertResponseBadFinishReasonToErrorMsg( - response: EnhancedGenerateContentResponse -): string | null { - if (response.candidates && response.candidates.length > 0) { - const candidate = response.candidates[0]; - if (hadBadFinishReason(candidate)) { - if ( - candidate.finishReason === FinishReason.SAFETY && - candidate.safetyRatings && - (candidate.safetyRatings?.length ?? 0) > 0 - ) { - const safetyReasons = getSafetyReasons(candidate.safetyRatings as SafetyReason[]); - return `ActionsClientGeminiChatModel: action result status is error. Candidate was blocked due to ${candidate.finishReason} - ${safetyReasons}`; - } else { - return `ActionsClientGeminiChatModel: action result status is error. Candidate was blocked due to ${candidate.finishReason}`; - } - } - } - return null; -} - -const getSafetyReasons = (safetyRatings: SafetyReason[]) => { - const reasons = safetyRatings.filter((t: SafetyReason) => t.blocked); - return reasons.reduce( - (acc: string, t: SafetyReason, i: number) => - `${acc.length ? `${acc} ` : ''}${t.category}: ${t.severity}${ - i < reasons.length - 1 ? ',' : '' - }`, - '' - ); -}; diff --git a/x-pack/packages/kbn-langchain/server/language_models/index.ts b/x-pack/packages/kbn-langchain/server/language_models/index.ts index f5415079cbc11..b3c5053f11701 100644 --- a/x-pack/packages/kbn-langchain/server/language_models/index.ts +++ b/x-pack/packages/kbn-langchain/server/language_models/index.ts @@ -8,5 +8,6 @@ export { ActionsClientBedrockChatModel } from './bedrock_chat'; export { ActionsClientChatOpenAI } from './chat_openai'; export { ActionsClientGeminiChatModel } from './gemini_chat'; +export { ActionsClientChatVertexAI } from './chat_vertex'; export { ActionsClientLlm } from './llm'; export { ActionsClientSimpleChatModel } from './simple_chat_model'; diff --git a/x-pack/packages/kbn-langchain/server/utils/gemini.ts b/x-pack/packages/kbn-langchain/server/utils/gemini.ts index 7d08923083831..9cc975d8c1f2b 100644 --- a/x-pack/packages/kbn-langchain/server/utils/gemini.ts +++ b/x-pack/packages/kbn-langchain/server/utils/gemini.ts @@ -6,9 +6,306 @@ */ import { Logger } from '@kbn/core/server'; +import { + Content, + EnhancedGenerateContentResponse, + FinishReason, + FunctionCallPart, + FunctionResponsePart, + InlineDataPart, + Part, + POSSIBLE_ROLES, + SafetyRating, + TextPart, +} from '@google/generative-ai'; +import { + AIMessageChunk, + BaseMessage, + ChatMessage, + isBaseMessage, + UsageMetadata, +} from '@langchain/core/messages'; +import { ChatGenerationChunk } from '@langchain/core/outputs'; +import { ToolCallChunk } from '@langchain/core/dist/messages/tool'; import { Readable } from 'stream'; import { StreamParser } from './types'; +export function convertResponseContentToChatGenerationChunk( + response: EnhancedGenerateContentResponse, + extra: { + usageMetadata?: UsageMetadata | undefined; + index: number; + } +): ChatGenerationChunk | null { + if (!response.candidates || response.candidates.length === 0) { + return null; + } + const functionCalls = response.functionCalls(); + const [candidate] = response.candidates; + const { content, ...generationInfo } = candidate; + const text = content?.parts[0]?.text ?? ''; + + const toolCallChunks: ToolCallChunk[] = []; + if (functionCalls) { + toolCallChunks.push( + ...functionCalls.map((fc) => ({ + ...fc, + args: JSON.stringify(fc.args), + index: extra.index, + type: 'tool_call_chunk' as const, + })) + ); + } + return new ChatGenerationChunk({ + text, + message: new AIMessageChunk({ + content: text, + name: !content ? undefined : content.role, + tool_call_chunks: toolCallChunks, + // Each chunk can have unique "generationInfo", and merging strategy is unclear, + // so leave blank for now. + additional_kwargs: {}, + usage_metadata: extra.usageMetadata, + }), + generationInfo, + }); +} + +export function convertAuthorToRole(author: string): (typeof POSSIBLE_ROLES)[number] { + switch (author) { + /** + * Note: Gemini currently is not supporting system messages + * we will convert them to human messages and merge with following + * */ + case 'ai': + case 'model': // getMessageAuthor returns message.name. code ex.: return message.name ?? type; + return 'model'; + case 'system': + case 'human': + return 'user'; + case 'tool': + case 'function': + return 'function'; + default: + throw new Error(`Unknown / unsupported author: ${author}`); + } +} +export function convertBaseMessagesToContent(messages: BaseMessage[], isMultimodalModel: boolean) { + return messages.reduce<{ + content: Content[]; + mergeWithPreviousContent: boolean; + }>( + (acc, message, index) => { + if (!isBaseMessage(message)) { + throw new Error('Unsupported message input'); + } + const author = getMessageAuthor(message); + if (author === 'system' && index !== 0) { + throw new Error('System message should be the first one'); + } + const role = convertAuthorToRole(author); + const parts = convertMessageContentToParts(message, isMultimodalModel); + + if (acc.mergeWithPreviousContent) { + const prevContent = acc.content[acc.content.length - 1]; + if (!prevContent) { + throw new Error( + 'There was a problem parsing your system message. Please try a prompt without one.' + ); + } + prevContent.parts.push(...parts); + + return { + mergeWithPreviousContent: false, + content: acc.content, + }; + } + let actualRole = role; + if (actualRole === 'function') { + // GenerativeAI API will throw an error if the role is not "user" or "model." + actualRole = 'user'; + } + const content: Content = { + role: actualRole, + parts, + }; + return { + mergeWithPreviousContent: author === 'system', + content: [...acc.content, content], + }; + }, + { content: [], mergeWithPreviousContent: false } + ).content; +} + +export function convertMessageContentToParts( + message: BaseMessage, + isMultimodalModel: boolean +): Part[] { + if (typeof message.content === 'string' && message.content !== '') { + return [{ text: message.content }]; + } + + let functionCalls: FunctionCallPart[] = []; + let functionResponses: FunctionResponsePart[] = []; + let messageParts: Part[] = []; + + if ( + 'tool_calls' in message && + Array.isArray(message.tool_calls) && + message.tool_calls.length > 0 + ) { + functionCalls = message.tool_calls.map((tc) => ({ + functionCall: { + name: tc.name, + args: tc.args, + }, + })); + } else if (message._getType() === 'tool' && message.name && message.content) { + functionResponses = [ + { + functionResponse: { + name: message.name, + response: message.content, + }, + }, + ]; + } else if (Array.isArray(message.content)) { + messageParts = message.content.map((c) => { + if (c.type === 'text') { + return { + text: c.text, + } as TextPart; + } + + if (c.type === 'image_url') { + if (!isMultimodalModel) { + throw new Error(`This model does not support images`); + } + let source; + if (typeof c.image_url === 'string') { + source = c.image_url; + } else if (typeof c.image_url === 'object' && 'url' in c.image_url) { + source = c.image_url.url; + } else { + throw new Error('Please provide image as base64 encoded data URL'); + } + const [dm, data] = source.split(','); + if (!dm.startsWith('data:')) { + throw new Error('Please provide image as base64 encoded data URL'); + } + + const [mimeType, encoding] = dm.replace(/^data:/, '').split(';'); + if (encoding !== 'base64') { + throw new Error('Please provide image as base64 encoded data URL'); + } + + return { + inlineData: { + data, + mimeType, + }, + } as InlineDataPart; + } else if (c.type === 'media') { + return messageContentMedia(c); + } else if (c.type === 'tool_use') { + return { + functionCall: { + name: c.name, + args: c.input, + }, + } as FunctionCallPart; + } + throw new Error(`Unknown content type ${(c as { type: string }).type}`); + }); + } + + return [...messageParts, ...functionCalls, ...functionResponses]; +} + +export function getMessageAuthor(message: BaseMessage) { + const type = message._getType(); + if (ChatMessage.isInstance(message)) { + return message.role; + } + if (type === 'tool') { + return type; + } + return message.name ?? type; +} + +// will be removed once FileDataPart is supported in @langchain/google-genai +function messageContentMedia(content: Record): InlineDataPart { + if ('mimeType' in content && 'data' in content) { + return { + inlineData: { + mimeType: content.mimeType, + data: content.data, + }, + } as InlineDataPart; + } + throw new Error('Invalid media content'); +} + +// TODO Google's TS library is behind the API +// remove this enum once the library is updated +// https://github.com/google-gemini/generative-ai-js/pull/270 +enum FinishReasonMore { + BLOCKLIST = 'BLOCKLIST', + PROHIBITED_CONTENT = 'PROHIBITED_CONTENT', + SPII = 'SPII', + MALFORMED_FUNCTION_CALL = 'MALFORMED_FUNCTION_CALL', +} +const badFinishReasons = [ + FinishReason.RECITATION, + FinishReason.SAFETY, + FinishReasonMore.BLOCKLIST, + FinishReasonMore.PROHIBITED_CONTENT, + FinishReasonMore.SPII, + FinishReasonMore.MALFORMED_FUNCTION_CALL, +]; +function hadBadFinishReason(candidate: { finishReason?: FinishReason }) { + return !!candidate.finishReason && badFinishReasons.includes(candidate.finishReason); +} + +export function convertResponseBadFinishReasonToErrorMsg( + response: EnhancedGenerateContentResponse +): string | null { + if (response.candidates && response.candidates.length > 0) { + const candidate = response.candidates[0]; + if (hadBadFinishReason(candidate)) { + if ( + candidate.finishReason === FinishReason.SAFETY && + candidate.safetyRatings && + (candidate.safetyRatings?.length ?? 0) > 0 + ) { + const safetyReasons = getSafetyReasons(candidate.safetyRatings as SafetyReason[]); + return `Gemini Utils: action result status is error. Candidate was blocked due to ${candidate.finishReason} - ${safetyReasons}`; + } else { + return `Gemini Utils: action result status is error. Candidate was blocked due to ${candidate.finishReason}`; + } + } + } + return null; +} + +// not sure why these properties are not on the type, as they are on the data +interface SafetyReason extends SafetyRating { + blocked: boolean; + severity: string; +} + +const getSafetyReasons = (safetyRatings: SafetyReason[]) => { + const reasons = safetyRatings.filter((t: SafetyReason) => t.blocked); + return reasons.reduce( + (acc: string, t: SafetyReason, i: number) => + `${acc.length ? `${acc} ` : ''}${t.category}: ${t.severity}${ + i < reasons.length - 1 ? ',' : '' + }`, + '' + ); +}; + export const parseGeminiStreamAsAsyncIterator = async function* ( stream: Readable, logger: Logger, diff --git a/x-pack/performance/journeys_e2e/many_fields_discover.ts b/x-pack/performance/journeys_e2e/many_fields_discover.ts index 1164cfbff1a8b..4943e2f59c413 100644 --- a/x-pack/performance/journeys_e2e/many_fields_discover.ts +++ b/x-pack/performance/journeys_e2e/many_fields_discover.ts @@ -9,8 +9,6 @@ import { Journey } from '@kbn/journeys'; import { subj } from '@kbn/test-subj-selector'; export const journey = new Journey({ - // Failing: See https://github.com/elastic/kibana/issues/193305 - skipped: true, kbnArchives: ['test/functional/fixtures/kbn_archiver/many_fields_data_view'], esArchives: ['test/functional/fixtures/es_archiver/many_fields'], }) @@ -24,9 +22,9 @@ export const journey = new Journey({ await page.waitForSelector('[data-test-subj="discoverDocTable"][data-render-complete="true"]'); await page.waitForSelector(subj('globalLoadingIndicator-hidden')); }) - .step('Expand the first document', async ({ page }) => { + .step('Expand a document', async ({ page }) => { const expandButtons = page.locator(subj('docTableExpandToggleColumn')); - await expandButtons.last().click(); + await expandButtons.nth(3).click(); await page.waitForSelector(subj('docTableRowAction')); await page.click(subj('docTableRowAction')); await page.waitForSelector(subj('globalLoadingIndicator-hidden')); diff --git a/x-pack/plugins/data_quality/common/url_schema/dataset_quality_details_url_schema_v1.ts b/x-pack/plugins/data_quality/common/url_schema/dataset_quality_details_url_schema_v1.ts index e177a325b353b..97c7771bbf994 100644 --- a/x-pack/plugins/data_quality/common/url_schema/dataset_quality_details_url_schema_v1.ts +++ b/x-pack/plugins/data_quality/common/url_schema/dataset_quality_details_url_schema_v1.ts @@ -19,6 +19,7 @@ export const urlSchemaRT = rt.exact( breakdownField: rt.string, degradedFields: degradedFieldRT, expandedDegradedField: rt.string, + showCurrentQualityIssues: rt.boolean, }), ]) ); diff --git a/x-pack/plugins/data_quality/public/routes/dataset_quality_details/url_schema_v1.ts b/x-pack/plugins/data_quality/public/routes/dataset_quality_details/url_schema_v1.ts index 1b08d9832c093..7b91895598eca 100644 --- a/x-pack/plugins/data_quality/public/routes/dataset_quality_details/url_schema_v1.ts +++ b/x-pack/plugins/data_quality/public/routes/dataset_quality_details/url_schema_v1.ts @@ -19,6 +19,7 @@ export const getStateFromUrlValue = ( degradedFields: urlValue.degradedFields, breakdownField: urlValue.breakdownField, expandedDegradedField: urlValue.expandedDegradedField, + showCurrentQualityIssues: urlValue.showCurrentQualityIssues, }); export const getUrlValueFromState = ( @@ -30,6 +31,7 @@ export const getUrlValueFromState = ( degradedFields: state.degradedFields, breakdownField: state.breakdownField, expandedDegradedField: state.expandedDegradedField, + showCurrentQualityIssues: state.showCurrentQualityIssues, v: 1, }); diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/generate_chat_title.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/generate_chat_title.ts index dcef2ae6345f4..47a36ddf844b0 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/generate_chat_title.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/generate_chat_title.ts @@ -28,7 +28,7 @@ export const GENERATE_CHAT_TITLE_PROMPT = (responseLanguage: string, llmType?: s ? ChatPromptTemplate.fromMessages([ [ 'system', - `You are a title generator for a helpful assistant for Elastic Security. Assume the following human message is the start of a conversation between you and a human; Do not respond to the human message, instead respond with conversation title relevant to the human's message. DO NOT UNDER ANY CIRCUMSTANCES use quotes or markdown in your response. This title is shown in a list of conversations to the human, so title it for the user, not for you. Please create the title in ${responseLanguage}. Respond with the title only with no other text explaining your response. As an example, for the given MESSAGE, this is the TITLE: + `You are a title generator for a helpful assistant for Elastic Security. Assume the following human message is the start of a conversation between you and a human. Generate a relevant conversation title for the human's message in plain text. Make sure the title is formatted for the user, without using quotes or markdown. The title should clearly reflect the content of the message and be appropriate for a list of conversations. Please create the title in ${responseLanguage}. Respond only with the title. As an example, for the given MESSAGE, this is the TITLE: MESSAGE: I am having trouble with the Elastic Security app. TITLE: Troubleshooting Elastic Security app issues diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts index 36c15aa44445d..2d076f6bd1472 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/run_agent.ts @@ -7,6 +7,7 @@ import { RunnableConfig } from '@langchain/core/runnables'; import { AgentRunnableSequence } from 'langchain/dist/agents/agent'; +import { formatLatestUserMessage } from '../prompts'; import { AgentState, NodeParamsBase } from '../types'; import { NodeType } from '../constants'; @@ -37,6 +38,8 @@ export async function runAgent({ const agentOutcome = await agentRunnable.withConfig({ tags: [AGENT_NODE_TAG] }).invoke( { ...state, + // prepend any user prompt (gemini) + input: formatLatestUserMessage(state.input, state.llmType), chat_history: state.messages, // TODO: Message de-dupe with ...state spread }, config diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts index ae8e3c18c2217..9eedce48ba69d 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/nodes/translations.ts @@ -10,8 +10,11 @@ const YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT = const IF_YOU_DONT_KNOW_THE_ANSWER = 'Do not answer questions unrelated to Elastic Security.'; export const DEFAULT_SYSTEM_PROMPT = `${YOU_ARE_A_HELPFUL_EXPERT_ASSISTANT} ${IF_YOU_DONT_KNOW_THE_ANSWER}`; - -export const GEMINI_SYSTEM_PROMPT = - `ALWAYS use the provided tools, as they have access to the latest data and syntax.` + - "The final response is the only output the user sees and should be a complete answer to the user's question. Do not leave out important tool output. The final response should never be empty. Don't forget to use tools."; +// system prompt from @afirstenberg +const BASE_GEMINI_PROMPT = + 'You are an assistant that is an expert at using tools and Elastic Security, doing your best to use these tools to answer questions or follow instructions. It is very important to use tools to answer the question or follow the instructions rather than coming up with your own answer. Tool calls are good. Sometimes you may need to make several tool calls to accomplish the task or get an answer to the question that was asked. Use as many tool calls as necessary.'; +const KB_CATCH = + 'If the knowledge base tool gives empty results, do your best to answer the question from the perspective of an expert security analyst.'; +export const GEMINI_SYSTEM_PROMPT = `${BASE_GEMINI_PROMPT} ${KB_CATCH}`; export const BEDROCK_SYSTEM_PROMPT = `Use tools as often as possible, as they have access to the latest data and syntax. Always return value from ESQLKnowledgeBaseTool as is. Never return tags in the response, but make sure to include tags content in the response. Do not reflect on the quality of the returned search results in your response.`; +export const GEMINI_USER_PROMPT = `Now, always using the tools at your disposal, step by step, come up with a response to this request:\n\n`; diff --git a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts index eb52c227421fc..4a7b1fd46ccb8 100644 --- a/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts +++ b/x-pack/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/prompts.ts @@ -10,6 +10,7 @@ import { BEDROCK_SYSTEM_PROMPT, DEFAULT_SYSTEM_PROMPT, GEMINI_SYSTEM_PROMPT, + GEMINI_USER_PROMPT, } from './nodes/translations'; export const formatPrompt = (prompt: string, additionalPrompt?: string) => @@ -23,7 +24,8 @@ export const formatPrompt = (prompt: string, additionalPrompt?: string) => export const systemPrompts = { openai: DEFAULT_SYSTEM_PROMPT, bedrock: `${DEFAULT_SYSTEM_PROMPT} ${BEDROCK_SYSTEM_PROMPT}`, - gemini: `${DEFAULT_SYSTEM_PROMPT} ${GEMINI_SYSTEM_PROMPT}`, + // The default prompt overwhelms gemini, do not prepend + gemini: GEMINI_SYSTEM_PROMPT, structuredChat: `Respond to the human as helpfully and accurately as possible. You have access to the following tools: {tools} @@ -98,3 +100,16 @@ export const formatPromptStructured = (prompt: string, additionalPrompt?: string ]); export const structuredChatAgentPrompt = formatPromptStructured(systemPrompts.structuredChat); + +/** + * If Gemini is the llmType, + * Adds a user prompt for the latest message in a conversation + * @param prompt + * @param llmType + */ +export const formatLatestUserMessage = (prompt: string, llmType?: string): string => { + if (llmType === 'gemini') { + return `${GEMINI_USER_PROMPT}${prompt}`; + } + return prompt; +}; diff --git a/x-pack/plugins/elastic_assistant/server/routes/utils.ts b/x-pack/plugins/elastic_assistant/server/routes/utils.ts index e163526d996ae..651a809e1a56e 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/utils.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/utils.ts @@ -17,7 +17,7 @@ import { ActionsClientChatOpenAI, ActionsClientBedrockChatModel, ActionsClientSimpleChatModel, - ActionsClientGeminiChatModel, + ActionsClientChatVertexAI, } from '@kbn/langchain/server'; import { CustomHttpRequestError } from './custom_http_request_error'; @@ -187,5 +187,5 @@ export const getLlmClass = (llmType?: string, bedrockChatEnabled?: boolean) => : llmType === 'bedrock' && bedrockChatEnabled ? ActionsClientBedrockChatModel : llmType === 'gemini' && bedrockChatEnabled - ? ActionsClientGeminiChatModel + ? ActionsClientChatVertexAI : ActionsClientSimpleChatModel; diff --git a/x-pack/plugins/elastic_assistant/server/types.ts b/x-pack/plugins/elastic_assistant/server/types.ts index 58ee4a9dd4545..af8d019539a66 100755 --- a/x-pack/plugins/elastic_assistant/server/types.ts +++ b/x-pack/plugins/elastic_assistant/server/types.ts @@ -39,6 +39,7 @@ import { import { ActionsClientBedrockChatModel, ActionsClientChatOpenAI, + ActionsClientChatVertexAI, ActionsClientGeminiChatModel, ActionsClientLlm, ActionsClientSimpleChatModel, @@ -230,6 +231,7 @@ export type AssistantToolLlm = | ActionsClientBedrockChatModel | ActionsClientChatOpenAI | ActionsClientGeminiChatModel + | ActionsClientChatVertexAI | ActionsClientSimpleChatModel; export interface AssistantToolParams { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_detail.tsx index 645090ee4b777..4c787e9ef28ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/connector_detail/connector_detail.tsx @@ -121,7 +121,7 @@ export const ConnectorDetail: React.FC = () => { label: i18n.translate( 'xpack.enterpriseSearch.content.connectors.connectorDetail.indexMappingsTabLabel', { - defaultMessage: 'Index mappings', + defaultMessage: 'Mappings', } ), onClick: () => diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/access_control_index_selector/access_control_index_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/access_control_index_selector/access_control_index_selector.tsx index c525ed0a413af..e02741638f863 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/access_control_index_selector/access_control_index_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/components/access_control_index_selector/access_control_index_selector.tsx @@ -7,21 +7,36 @@ import React from 'react'; -import { EuiFlexGrid, EuiFlexItem, EuiSuperSelect, EuiText, EuiTitle } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiSuperSelect, + EuiText, + EuiTitle, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; export interface AccessControlSelectorOption { description: string; + error?: boolean; title: string; value: 'content-index' | 'access-control-index'; } -const indexSelectorOptions: AccessControlSelectorOption[] = [ +interface IndexSelectorProps { + fullWidth?: boolean; + indexSelectorOptions?: AccessControlSelectorOption[]; + onChange(value: AccessControlSelectorOption['value']): void; + valueOfSelected?: AccessControlSelectorOption['value']; +} + +export const DEFAULT_INDEX_SELECTOR_OPTIONS: AccessControlSelectorOption[] = [ { description: i18n.translate( 'xpack.enterpriseSearch.content.searchIndex.documents.selector.contentIndex.description', { - defaultMessage: 'Browse content fields', + defaultMessage: 'Browse documents ingested by content syncs', } ), title: i18n.translate( @@ -36,7 +51,7 @@ const indexSelectorOptions: AccessControlSelectorOption[] = [ description: i18n.translate( 'xpack.enterpriseSearch.content.searchIndex.documents.selector.accessControl.description', { - defaultMessage: 'Browse document level security fields', + defaultMessage: 'Browse access control lists ingested by access control syncs', } ), title: i18n.translate( @@ -49,34 +64,44 @@ const indexSelectorOptions: AccessControlSelectorOption[] = [ }, ]; -interface IndexSelectorProps { - onChange(value: AccessControlSelectorOption['value']): void; - valueOfSelected?: AccessControlSelectorOption['value']; -} - export const AccessControlIndexSelector: React.FC = ({ - valueOfSelected, + indexSelectorOptions = DEFAULT_INDEX_SELECTOR_OPTIONS, onChange, + valueOfSelected, + fullWidth, }) => { return ( option.error) ? ( + + ) : undefined + } options={indexSelectorOptions.map((option) => { return { dropdownDisplay: ( - - - -

{option.title}

-
-
- - -

{option.description}

-
-
-
+ + {option.error ? ( + + {' '} + + ) : null} + + + +

{option.title}

+
+
+ + +

{option.description}

+
+
+
+
), inputDisplay: option.title, value: option.value, diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/sync_rules/connector_rules.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/sync_rules/connector_rules.tsx index 2607cea17ce6c..6791e7778c9a3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/sync_rules/connector_rules.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/connector/sync_rules/connector_rules.tsx @@ -56,7 +56,6 @@ export const ConnectorSyncRules: React.FC = () => { setIsEditing={setIsEditing} /> )} - {hasDraft && ( @@ -71,18 +70,6 @@ export const ConnectorSyncRules: React.FC = () => { - - - -

- {i18n.translate('xpack.enterpriseSearch.index.connector.syncRules.title', { - defaultMessage: 'Sync rules ', - })} -

-
-
-
-

{i18n.translate('xpack.enterpriseSearch.index.connector.syncRules.description', { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx index 592da5d044f2a..d384d218c0e25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/documents.tsx @@ -133,6 +133,7 @@ export const SearchIndexDocuments: React.FC = () => { accessControlSwitch={ shouldShowAccessControlSwitcher ? ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.scss index bdde0fedc412e..a52d3d75c851b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.scss +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.scss @@ -1,3 +1,3 @@ .enterpriseSearchMappingsSelector { - max-width: $euiSizeXXL * 6; + width: 100%; } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx index b35415cc2e0e0..f9f46e854becd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/index_mappings.tsx @@ -69,13 +69,13 @@ export const SearchIndexIndexMappings: React.FC = () => { return ( <> - {shouldShowAccessControlSwitch && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx index b05e2b66c37e8..40b7c9596cb3a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/pipelines.tsx @@ -110,7 +110,6 @@ export const SearchIndexPipelines: React.FC = () => { return ( <> - {showMissingPipelineCallout && ( <> { }), }, { - content: , + content: ( + <> + + + + ), id: SearchIndexTabId.DOCUMENTS, name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.documentsTabLabel', { defaultMessage: 'Documents', }), }, { - content: , + content: ( + <> + + + + ), id: SearchIndexTabId.INDEX_MAPPINGS, name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.indexMappingsTabLabel', { - defaultMessage: 'Index mappings', + defaultMessage: 'Mappings', }), }, ]; const CONNECTOR_TABS: EuiTabbedContentTab[] = [ { - content: , + content: ( + <> + + + + ), id: SearchIndexTabId.CONFIGURATION, name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.configurationTabLabel', { defaultMessage: 'Configuration', @@ -167,7 +182,12 @@ export const SearchIndex: React.FC = () => { ...(hasFilteringFeature ? [ { - content: , + content: ( + <> + + + + ), id: SearchIndexTabId.SYNC_RULES, name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.syncRulesTabLabel', { defaultMessage: 'Sync rules', @@ -176,7 +196,12 @@ export const SearchIndex: React.FC = () => { ] : []), { - content: , + content: ( + <> + + + + ), id: SearchIndexTabId.SCHEDULING, name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.schedulingTabLabel', { defaultMessage: 'Scheduling', @@ -186,14 +211,24 @@ export const SearchIndex: React.FC = () => { const CRAWLER_TABS: EuiTabbedContentTab[] = [ { - content: , + content: ( + <> + + + + ), id: SearchIndexTabId.DOMAIN_MANAGEMENT, name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.domainManagementTabLabel', { defaultMessage: 'Manage Domains', }), }, { - content: , + content: ( + <> + + + + ), id: SearchIndexTabId.CRAWLER_CONFIGURATION, name: i18n.translate( 'xpack.enterpriseSearch.content.searchIndex.crawlerConfigurationTabLabel', @@ -203,7 +238,12 @@ export const SearchIndex: React.FC = () => { ), }, { - content: , + content: ( + <> + + + + ), 'data-test-subj': 'entSearchContent-index-crawler-scheduler-tab', id: SearchIndexTabId.SCHEDULING, name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.schedulingTabLabel', { @@ -213,7 +253,12 @@ export const SearchIndex: React.FC = () => { ]; const PIPELINES_TAB: EuiTabbedContentTab = { - content: , + content: ( + <> + + + + ), id: SearchIndexTabId.PIPELINES, name: i18n.translate('xpack.enterpriseSearch.content.searchIndex.pipelinesTabLabel', { defaultMessage: 'Pipelines', @@ -289,7 +334,7 @@ const Content: React.FC = ({ } return ( <> - + {isCrawlerIndex(index) && } ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx index 9495716e1a5e0..2669457cb6837 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/sync_jobs/sync_jobs.tsx @@ -5,20 +5,25 @@ * 2.0. */ -import React, { useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; +import { css } from '@emotion/react'; import { useActions, useValues } from 'kea'; -import { EuiButtonGroup } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiSpacer, useEuiTheme } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; - import { Connector, SyncJobsTable } from '@kbn/search-connectors'; import { KibanaLogic } from '../../../../shared/kibana'; import { hasDocumentLevelSecurityFeature } from '../../../utils/connector_helpers'; +import { + AccessControlIndexSelector, + AccessControlSelectorOption, +} from '../components/access_control_index_selector/access_control_index_selector'; + import { SyncJobsViewLogic } from './sync_jobs_view_logic'; export interface SyncJobsProps { @@ -31,6 +36,8 @@ export const SyncJobs: React.FC = ({ connector }) => { productFeatures.hasDocumentLevelSecurityEnabled && hasDocumentLevelSecurityFeature(connector); const errorOnAccessSync = Boolean(connector.last_access_control_sync_error); const errorOnContentSync = Boolean(connector.last_sync_error); + const [selectedIndexType, setSelectedIndexType] = + useState('content-index'); const { connectorId, syncJobsPagination: pagination, @@ -63,44 +70,84 @@ export const SyncJobs: React.FC = ({ connector }) => { } }, [connectorId, selectedSyncJobCategory]); + useEffect(() => { + if (selectedIndexType === 'content-index') { + setSelectedSyncJobCategory('content'); + } else { + setSelectedSyncJobCategory('access_control'); + } + }, [selectedIndexType]); + const { euiTheme } = useEuiTheme(); return ( <> {shouldShowAccessSyncs && ( - { - if (optionId === 'content' || optionId === 'access_control') { - setSelectedSyncJobCategory(optionId); - } - }} - options={[ - { - id: 'content', - label: i18n.translate( - 'xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.content.label', - { defaultMessage: 'Content syncs' } - ), - ...(errorOnContentSync ? { iconSide: 'right', iconType: 'warning' } : {}), - }, - - { - id: 'access_control', - label: i18n.translate( - 'xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.accessControl.label', - { defaultMessage: 'Access control syncs' } - ), - ...(errorOnAccessSync ? { iconSide: 'right', iconType: 'warning' } : {}), - }, - ]} - /> + <> + + + + + + + {i18n.translate( + 'xpack.enterpriseSearch.accessControlIndexSelector.p.accessControlSyncsAreLabel', + { + defaultMessage: + 'Access control syncs keep permissions information up to date for document level security (DLS)', + } + )} +

+ } + position="right" + /> +
+
+ + )} {selectedSyncJobCategory === 'content' ? ( { }, asInternalUser: {}, }; + + const getSearchProviderContext = ({ + enterpriseSearchEnabled, + }: { + enterpriseSearchEnabled: boolean; + }): GlobalSearchProviderContext => ({ + core: { + capabilities: of({ + catalogue: { enterpriseSearch: enterpriseSearchEnabled }, + management: {}, + navLinks: {}, + }), + savedObjects: {} as any, + uiSettings: {} as any, + }, + }); + const mockSearchProviderContext = getSearchProviderContext({ enterpriseSearchEnabled: true }); + afterEach(() => { jest.clearAllMocks(); }); @@ -74,7 +94,7 @@ describe('Enterprise Search - connectors search provider', () => { client, preference: '', }, - {} as any + mockSearchProviderContext ) ); expect(results).toEqual([{ ...getConnectorSearchData('postgres'), score: 100 }]); @@ -90,7 +110,7 @@ describe('Enterprise Search - connectors search provider', () => { maxResults: 100, preference: '', }, - {} as any + mockSearchProviderContext ) ); expect(results).toEqual([ @@ -110,7 +130,7 @@ describe('Enterprise Search - connectors search provider', () => { maxResults: 100, preference: '', }, - {} as any + mockSearchProviderContext ) ); expect(results).toHaveLength(0); @@ -126,7 +146,7 @@ describe('Enterprise Search - connectors search provider', () => { maxResults: 1, preference: '', }, - {} as any + mockSearchProviderContext ) ); expect(results).toEqual([{ ...getConnectorSearchData('postgres'), score: 90 }]); @@ -143,7 +163,7 @@ describe('Enterprise Search - connectors search provider', () => { maxResults: 100, preference: '', }, - {} as any + mockSearchProviderContext ) ); expect(results).toEqual([]); @@ -159,7 +179,7 @@ describe('Enterprise Search - connectors search provider', () => { maxResults: 100, preference: '', }, - {} as any + mockSearchProviderContext ) ); expect(results).toEqual([]); @@ -175,7 +195,7 @@ describe('Enterprise Search - connectors search provider', () => { maxResults: 100, preference: '', }, - {} as any + mockSearchProviderContext ) ); expect(results).toEqual([]); @@ -191,7 +211,23 @@ describe('Enterprise Search - connectors search provider', () => { maxResults: 100, preference: '', }, - {} as any + mockSearchProviderContext + ) + ); + expect(results).toEqual([]); + }); + + it('if capabilities.catalogue.enterpriseSearch is false', async () => { + const results = await lastValueFrom( + connectorsSearchResultProvider.find( + { term: 'companyName-postgres-connector-all' }, + { + aborted$: NEVER, + client, + maxResults: 100, + preference: '', + }, + getSearchProviderContext({ enterpriseSearchEnabled: false }) ) ); expect(results).toEqual([]); diff --git a/x-pack/plugins/enterprise_search/server/utils/connectors_search_result_provider.ts b/x-pack/plugins/enterprise_search/server/utils/connectors_search_result_provider.ts index 7e81d5b6d09ec..09386a4a30300 100644 --- a/x-pack/plugins/enterprise_search/server/utils/connectors_search_result_provider.ts +++ b/x-pack/plugins/enterprise_search/server/utils/connectors_search_result_provider.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { from, takeUntil } from 'rxjs'; +import { from, takeUntil, switchMap, of } from 'rxjs'; import type { IStaticAssets } from '@kbn/core-http-browser'; @@ -25,7 +25,7 @@ export function getConnectorsSearchResultProvider( staticAssets: IStaticAssets ): GlobalSearchResultProvider { return { - find: ({ term, types, tags }, { aborted$, client, maxResults }) => { + find: ({ term, types, tags }, { aborted$, client, maxResults }, { core: { capabilities } }) => { if (!client || !term || tags || (types && !types.includes('connector'))) { return from([[]]); } @@ -52,7 +52,17 @@ export function getConnectorsSearchResultProvider( .slice(0, maxResults); return searchResults; }; - return from(getConnectorData()).pipe(takeUntil(aborted$)); + + return capabilities.pipe( + takeUntil(aborted$), + switchMap((caps) => { + if (!caps.catalogue.enterpriseSearch) { + return of([]); + } + + return from(getConnectorData()); + }) + ); }, getSearchableTypes: () => ['connector'], id: 'enterpriseSearchConnectors', diff --git a/x-pack/plugins/enterprise_search/server/utils/search_result_provider.test.ts b/x-pack/plugins/enterprise_search/server/utils/search_result_provider.test.ts index ef6e3f665b55a..49e94e76cd7a0 100644 --- a/x-pack/plugins/enterprise_search/server/utils/search_result_provider.test.ts +++ b/x-pack/plugins/enterprise_search/server/utils/search_result_provider.test.ts @@ -5,9 +5,11 @@ * 2.0. */ -import { NEVER } from 'rxjs'; +import { NEVER, of } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; +import type { GlobalSearchProviderContext } from '@kbn/global-search-plugin/server'; + import { ENTERPRISE_SEARCH_CONTENT_PLUGIN } from '../../common/constants'; import { getSearchResultProvider } from './search_result_provider'; @@ -18,6 +20,23 @@ const getTestScheduler = () => { }); }; +const getSearchProviderContext = ({ + enterpriseSearchEnabled, +}: { + enterpriseSearchEnabled: boolean; +}): GlobalSearchProviderContext => ({ + core: { + capabilities: of({ + catalogue: { enterpriseSearch: enterpriseSearchEnabled }, + management: {}, + navLinks: {}, + }), + savedObjects: {} as any, + uiSettings: {} as any, + }, +}); +const mockSearchProviderContext = getSearchProviderContext({ enterpriseSearchEnabled: true }); + const connectors = [ { categories: [ @@ -119,7 +138,7 @@ describe('Enterprise Search search provider', () => { searchResultProvider.find( { term: 'crawler' }, { aborted$: NEVER, maxResults: 100, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: [crawlerResult], @@ -133,7 +152,7 @@ describe('Enterprise Search search provider', () => { searchResultProvider.find( { term: '' }, { aborted$: NEVER, maxResults: 100, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: expect.arrayContaining([ @@ -150,7 +169,7 @@ describe('Enterprise Search search provider', () => { searchResultProvider.find( { term: '' }, { aborted$: NEVER, maxResults: 1, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: [{ ...crawlerResult, score: 80 }], @@ -173,7 +192,7 @@ describe('Enterprise Search search provider', () => { searchProvider.find( { term: '' }, { aborted$: NEVER, maxResults: 100, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: expect.not.arrayContaining([{ ...crawlerResult, score: 80 }]), @@ -196,7 +215,7 @@ describe('Enterprise Search search provider', () => { searchProvider.find( { term: '' }, { aborted$: NEVER, maxResults: 100, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: expect.not.arrayContaining([{ mongoResult, score: 80 }]), @@ -210,20 +229,34 @@ describe('Enterprise Search search provider', () => { searchResultProvider.find( { tags: ['tag'], term: '' }, { aborted$: NEVER, maxResults: 1, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: [], }); }); }); + it('returns nothing if unknown type is specified', () => { getTestScheduler().run(({ expectObservable }) => { expectObservable( searchResultProvider.find( { term: '', types: ['tag'] }, { aborted$: NEVER, maxResults: 1, preference: '' }, - {} as any + mockSearchProviderContext + ) + ).toBe('(a|)', { + a: [], + }); + }); + }); + it('returns nothing if capabilities.catalogue.enterpriseSearch is false', () => { + getTestScheduler().run(({ expectObservable }) => { + expectObservable( + searchResultProvider.find( + { term: '', types: ['tag'] }, + { aborted$: NEVER, maxResults: 1, preference: '' }, + getSearchProviderContext({ enterpriseSearchEnabled: false }) ) ).toBe('(a|)', { a: [], @@ -236,7 +269,7 @@ describe('Enterprise Search search provider', () => { searchResultProvider.find( { term: 'crawler', types: ['integration'] }, { aborted$: NEVER, maxResults: 1, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: [crawlerResult], @@ -249,7 +282,7 @@ describe('Enterprise Search search provider', () => { searchResultProvider.find( { term: 'crawler', types: ['enterprise search'] }, { aborted$: NEVER, maxResults: 1, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: [crawlerResult], @@ -272,7 +305,7 @@ describe('Enterprise Search search provider', () => { searchProvider.find( { term: 'app search' }, { aborted$: NEVER, maxResults: 1, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: [], @@ -295,7 +328,7 @@ describe('Enterprise Search search provider', () => { searchProvider.find( { term: 'workplace search' }, { aborted$: NEVER, maxResults: 1, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: [], @@ -318,7 +351,7 @@ describe('Enterprise Search search provider', () => { searchProvider.find( { term: '' }, { aborted$: NEVER, maxResults: 100, preference: '' }, - {} as any + mockSearchProviderContext ) ).toBe('(a|)', { a: expect.arrayContaining([ diff --git a/x-pack/plugins/enterprise_search/server/utils/search_result_provider.ts b/x-pack/plugins/enterprise_search/server/utils/search_result_provider.ts index bd01e16109f80..15b1971c6aecd 100644 --- a/x-pack/plugins/enterprise_search/server/utils/search_result_provider.ts +++ b/x-pack/plugins/enterprise_search/server/utils/search_result_provider.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { from, takeUntil } from 'rxjs'; +import { takeUntil, of, map } from 'rxjs'; import { GlobalSearchResultProvider } from '@kbn/global-search-plugin/server'; import { i18n } from '@kbn/i18n'; @@ -79,73 +79,84 @@ export function getSearchResultProvider( crawlerIconPath: string ): GlobalSearchResultProvider { return { - find: ({ term, types, tags }, { aborted$, maxResults }) => { + find: ({ term, types, tags }, { aborted$, maxResults }, { core: { capabilities } }) => { if ( tags || (types && !(types.includes('integration') || types.includes('enterprise search'))) ) { - return from([[]]); + return of([]); } - const services: ServiceDefinition[] = [ - ...(config.hasWebCrawler - ? [ - { - iconPath: crawlerIconPath, - keywords: ['crawler', 'web', 'website', 'internet', 'google'], - name: i18n.translate('xpack.enterpriseSearch.searchProvider.webCrawler.name', { - defaultMessage: 'Elastic Web Crawler', - }), - serviceType: ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE, - }, - ] - : []), - ...(config.hasConnectors ? connectorTypes : []), - ...(config.canDeployEntSearch - ? [ - { - keywords: ['esre', 'search'], - name: i18n.translate('xpack.enterpriseSearch.searchProvider.aiSearch.name', { - defaultMessage: 'Search AI', - }), - serviceType: 'ai_search', - url: AI_SEARCH_PLUGIN.URL, - }, - ] - : []), - ]; - const result = services - .map((service) => { - const { iconPath, isNative, keywords, name, serviceType } = service; - const url = 'url' in service ? service.url : undefined; - let score = 0; - const searchTerm = (term || '').toLowerCase(); - const searchName = name.toLowerCase(); - if (!searchTerm) { - score = 80; - } else if (searchName === searchTerm) { - score = 100; - } else if (searchName.startsWith(searchTerm)) { - score = 90; - } else if (searchName.includes(searchTerm)) { - score = 75; - } else if (serviceType === searchTerm) { - score = 65; - } else if (keywords.some((keyword) => keyword.includes(searchTerm))) { - score = 50; + + return capabilities.pipe( + takeUntil(aborted$), + map((caps) => { + if (!caps.catalogue.enterpriseSearch) { + return []; } - return toSearchResult({ - iconPath, - isCloud, - isNative, - name, - score, - serviceType, - url, - }); + + const services: ServiceDefinition[] = [ + ...(config.hasWebCrawler + ? [ + { + iconPath: crawlerIconPath, + keywords: ['crawler', 'web', 'website', 'internet', 'google'], + name: i18n.translate('xpack.enterpriseSearch.searchProvider.webCrawler.name', { + defaultMessage: 'Elastic Web Crawler', + }), + serviceType: ENTERPRISE_SEARCH_CONNECTOR_CRAWLER_SERVICE_TYPE, + }, + ] + : []), + ...(config.hasConnectors ? connectorTypes : []), + ...(config.canDeployEntSearch + ? [ + { + keywords: ['esre', 'search'], + name: i18n.translate('xpack.enterpriseSearch.searchProvider.aiSearch.name', { + defaultMessage: 'Search AI', + }), + serviceType: 'ai_search', + url: AI_SEARCH_PLUGIN.URL, + }, + ] + : []), + ]; + const result = services + .map((service) => { + const { iconPath, isNative, keywords, name, serviceType } = service; + const url = 'url' in service ? service.url : undefined; + let score = 0; + const searchTerm = (term || '').toLowerCase(); + const searchName = name.toLowerCase(); + if (!searchTerm) { + score = 80; + } else if (searchName === searchTerm) { + score = 100; + } else if (searchName.startsWith(searchTerm)) { + score = 90; + } else if (searchName.includes(searchTerm)) { + score = 75; + } else if (serviceType === searchTerm) { + score = 65; + } else if (keywords.some((keyword) => keyword.includes(searchTerm))) { + score = 50; + } + return toSearchResult({ + iconPath, + isCloud, + isNative, + name, + score, + serviceType, + url, + }); + }) + .filter(({ score }) => score > 0) + .slice(0, maxResults); + + return result; }) - .filter(({ score }) => score > 0) - .slice(0, maxResults); - return from([result]).pipe(takeUntil(aborted$)); + ); }, getSearchableTypes: () => ['enterprise search', 'integration'], id: 'enterpriseSearch', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts b/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts index 59512c6cded9a..c1496f424d393 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/built_in/services_from_ecs_data.ts @@ -20,7 +20,7 @@ const serviceTransactionFilter = (additionalFilters: string[] = []) => { export const builtInServicesFromEcsEntityDefinition: EntityDefinition = entityDefinitionSchema.parse({ - version: '0.2.0', + version: '0.3.0', id: `${BUILT_IN_ID_PREFIX}services_from_ecs_data`, name: 'Services from ECS data', description: @@ -46,8 +46,15 @@ export const builtInServicesFromEcsEntityDefinition: EntityDefinition = displayNameTemplate: '{{service.name}}', metadata: [ { source: '_index', destination: 'sourceIndex' }, + { + source: 'data_stream.type', + destination: 'source_data_stream.type', + }, + { + source: 'data_stream.dataset', + destination: 'source_data_stream.dataset', + }, { source: 'agent.name', aggregation: { type: 'terms', limit: 100 } }, - 'data_stream.type', 'service.environment', 'service.name', 'service.namespace', diff --git a/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts b/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts index 6932394a14e69..d1d84f27414af 100644 --- a/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts +++ b/x-pack/plugins/entity_manager/server/lib/entities/find_entity_definition.ts @@ -31,6 +31,7 @@ export async function findEntityDefinitions({ page = 1, perPage = 10, includeState = false, + type, }: { soClient: SavedObjectsClientContract; esClient: ElasticsearchClient; @@ -39,12 +40,14 @@ export async function findEntityDefinitions({ page?: number; perPage?: number; includeState?: boolean; + type?: string; }): Promise { const filter = compact([ typeof builtIn === 'boolean' ? `${SO_ENTITY_DEFINITION_TYPE}.attributes.id:(${BUILT_IN_ID_PREFIX}*)` : undefined, id ? `${SO_ENTITY_DEFINITION_TYPE}.attributes.id:(${id})` : undefined, + type ? `${SO_ENTITY_DEFINITION_TYPE}.attributes.type:(${type})` : undefined, ]).join(' AND '); const response = await soClient.find({ type: SO_ENTITY_DEFINITION_TYPE, diff --git a/x-pack/plugins/entity_manager/server/lib/entity_client.ts b/x-pack/plugins/entity_manager/server/lib/entity_client.ts index 5bd9154ec9daf..ee6b59b0ae0ea 100644 --- a/x-pack/plugins/entity_manager/server/lib/entity_client.ts +++ b/x-pack/plugins/entity_manager/server/lib/entity_client.ts @@ -75,11 +75,15 @@ export class EntityClient { page = 1, perPage = 10, includeState = false, + type, + builtIn, }: { id?: string; page?: number; perPage?: number; includeState?: boolean; + type?: string; + builtIn?: boolean; }) { const definitions = await findEntityDefinitions({ esClient: this.options.esClient, @@ -88,6 +92,8 @@ export class EntityClient { perPage, id, includeState, + type, + builtIn, }); return { definitions }; diff --git a/x-pack/plugins/observability_solution/apm/common/es_fields/entities.ts b/x-pack/plugins/observability_solution/apm/common/es_fields/entities.ts index 76e04769fe2b2..28e4a3ec79165 100644 --- a/x-pack/plugins/observability_solution/apm/common/es_fields/entities.ts +++ b/x-pack/plugins/observability_solution/apm/common/es_fields/entities.ts @@ -5,13 +5,8 @@ * 2.0. */ -export const ENTITY = 'entity'; -export const LAST_SEEN = 'entity.lastSeenTimestamp'; -export const FIRST_SEEN = 'entity.firstSeenTimestamp'; -export const ENTITY_ID = 'entity.id'; export const ENTITY_METRICS_LATENCY = 'entity.metrics.latency'; export const ENTITY_METRICS_LOG_ERROR_RATE = 'entity.metrics.logErrorRate'; export const ENTITY_METRICS_LOG_RATE = 'entity.metrics.logRate'; export const ENTITY_METRICS_THROUGHPUT = 'entity.metrics.throughput'; export const ENTITY_METRICS_FAILED_TRANSACTION_RATE = 'entity.metrics.failedTransactionRate'; -export const ENTITY_TYPE = 'entity.type'; diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/constants.ts b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/constants.ts index 497b779d24e7d..806a5d1dd1c28 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/constants.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/constants.ts @@ -4,5 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import { SettingsSpec } from '@elastic/charts'; + export const DEFAULT_DATE_FORMAT = 'HH:mm:ss'; export const CHART_ANNOTATION_RED_COLOR = '#BD271E'; +export const CHART_SETTINGS: Partial = { + showLegend: false, +}; diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/failed_transaction_chart.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/failed_transaction_chart.tsx index 02273f0f43141..f5031282ad7ad 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/failed_transaction_chart.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/failed_transaction_chart.tsx @@ -24,7 +24,7 @@ import { UI_SETTINGS } from '@kbn/data-plugin/public'; import { Theme } from '@elastic/charts'; import { AlertActiveTimeRangeAnnotation, AlertAnnotation } from '@kbn/observability-alert-details'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { DEFAULT_DATE_FORMAT } from './constants'; +import { CHART_SETTINGS, DEFAULT_DATE_FORMAT } from './constants'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { ChartType } from '../../../shared/charts/helper/get_timeseries_color'; import * as get_timeseries_color from '../../../shared/charts/helper/get_timeseries_color'; @@ -226,6 +226,7 @@ function FailedTransactionChart({ comparisonEnabled={false} customTheme={comparisonChartTheme} timeZone={timeZone} + settings={CHART_SETTINGS} />
diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/index.tsx index b9a7f4e8d8254..ada563851bdcc 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/index.tsx @@ -5,9 +5,15 @@ * 2.0. */ +import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; +import { COMPARATORS } from '@kbn/alerting-comparators'; +import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { formatAlertEvaluationValue } from '@kbn/observability-plugin/public'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { formatAlertEvaluationValue, Threshold } from '@kbn/observability-plugin/public'; +import { useChartThemes } from '@kbn/observability-shared-plugin/public'; +import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; import { ALERT_END, ALERT_EVALUATION_THRESHOLD, @@ -15,9 +21,6 @@ import { ALERT_RULE_TYPE_ID, ALERT_START, } from '@kbn/rule-data-utils'; -import React, { useEffect } from 'react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; import { EuiCallOut } from '@elastic/eui'; import { CoreStart } from '@kbn/core/public'; import { @@ -36,12 +39,7 @@ import ThroughputChart from './throughput_chart'; import { AlertDetailsAppSectionProps } from './types'; import { createCallApmApi } from '../../../../services/rest/create_call_apm_api'; -export function AlertDetailsAppSection({ - rule, - alert, - timeZone, - setAlertSummaryFields, -}: AlertDetailsAppSectionProps) { +export function AlertDetailsAppSection({ rule, alert, timeZone }: AlertDetailsAppSectionProps) { const { services } = useKibana(); createCallApmApi(services as CoreStart); @@ -54,42 +52,25 @@ export function AlertDetailsAppSection({ const transactionName = alert.fields[TRANSACTION_NAME]; const transactionType = alert.fields[TRANSACTION_TYPE]; - useEffect(() => { - const alertSummaryFields = [ - { - label: ( - - ), - value: formatAlertEvaluationValue(alertRuleTypeId, alertEvaluationValue), - }, - { - label: ( - - ), - value: formatAlertEvaluationValue(alertRuleTypeId, alertEvaluationThreshold), - }, - ]; - setAlertSummaryFields(alertSummaryFields); - }, [ - alertRuleTypeId, - alertEvaluationValue, - alertEvaluationThreshold, - environment, - serviceName, - transactionName, - setAlertSummaryFields, - ]); - const params = rule.params; const latencyAggregationType = getAggsTypeFromRule(params.aggregationType); const timeRange = getPaddedAlertTimeRange(alert.fields[ALERT_START]!, alert.fields[ALERT_END]); const comparisonChartTheme = getComparisonChartTheme(); + const chartThemes = useChartThemes(); + const thresholdComponent = + alertEvaluationValue && alertEvaluationThreshold ? ( + String(formatAlertEvaluationValue(alertRuleTypeId, d))} + title={i18n.translate('xpack.apm.alertDetails.thresholdTitle', { + defaultMessage: 'Threshold breached', + })} + comparator={COMPARATORS.GREATER_THAN} + /> + ) : undefined; const { from, to } = timeRange; if (!from || !to) { @@ -138,6 +119,7 @@ export function AlertDetailsAppSection({ latencyAggregationType={latencyAggregationType} comparisonEnabled={false} offset={''} + threshold={thresholdComponent} /> diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/latency_chart.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/latency_chart.tsx index 82fa091fa6617..74468c5152ec5 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/latency_chart.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/latency_chart.tsx @@ -7,7 +7,7 @@ import { Theme } from '@elastic/charts'; import { RecursivePartial } from '@elastic/eui'; -import React, { useMemo } from 'react'; +import React, { useMemo, ReactElement } from 'react'; import { EuiFlexItem, EuiPanel, EuiFlexGroup, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { BoolQuery } from '@kbn/es-query'; @@ -38,7 +38,7 @@ import { LatencyAggregationType } from '../../../../../common/latency_aggregatio import { isLatencyThresholdRuleType } from './helpers'; import { ApmDocumentType } from '../../../../../common/document_type'; import { usePreferredDataSourceAndBucketSize } from '../../../../hooks/use_preferred_data_source_and_bucket_size'; -import { DEFAULT_DATE_FORMAT } from './constants'; +import { CHART_SETTINGS, DEFAULT_DATE_FORMAT } from './constants'; import { TransactionTypeSelect } from './transaction_type_select'; import { ViewInAPMButton } from './view_in_apm_button'; @@ -61,6 +61,7 @@ function LatencyChart({ customAlertEvaluationThreshold, kuery = '', filters, + threshold, }: { alert: TopAlert; transactionType: string; @@ -78,6 +79,7 @@ function LatencyChart({ offset: string; timeZone: string; customAlertEvaluationThreshold?: number; + threshold?: ReactElement; kuery?: string; filters?: BoolQuery; }) { @@ -245,18 +247,28 @@ function LatencyChart({
- + + {!!threshold && ( + + {threshold} + + )} + + + + ); diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/throughput_chart.tsx b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/throughput_chart.tsx index b9eb0a4c729d0..f70de0861bb28 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/throughput_chart.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/throughput_chart.tsx @@ -17,6 +17,7 @@ import { EuiIconTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { CHART_SETTINGS } from './constants'; import { ChartType, getTimeSeriesColor } from '../../../shared/charts/helper/get_timeseries_color'; import { useFetcher } from '../../../../hooks/use_fetcher'; @@ -190,6 +191,7 @@ function ThroughputChart({ timeseries={timeseriesThroughput} yLabelFormat={asExactTransactionRate} timeZone={timeZone} + settings={CHART_SETTINGS} /> diff --git a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts index a5c692de0584d..f1a05a285d16d 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts @@ -6,7 +6,7 @@ */ import type { Rule } from '@kbn/alerting-plugin/common'; -import type { TopAlert, AlertSummaryField } from '@kbn/observability-plugin/public'; +import type { TopAlert } from '@kbn/observability-plugin/public'; import type { TIME_UNITS } from '@kbn/triggers-actions-ui-plugin/public'; import type { SERVICE_NAME, @@ -28,5 +28,4 @@ export interface AlertDetailsAppSectionProps { [SERVICE_ENVIRONMENT]: string; }>; timeZone: string; - setAlertSummaryFields: React.Dispatch>; } diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeseries_chart.tsx index c3654ea41f251..7b90aeb3ee03c 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/shared/charts/timeseries_chart.tsx @@ -23,6 +23,7 @@ import { XYBrushEvent, XYChartSeriesIdentifier, Tooltip, + SettingsSpec, } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -46,11 +47,13 @@ const END_ZONE_LABEL = i18n.translate('xpack.apm.timeseries.endzone', { defaultMessage: 'The selected time range does not include this entire bucket. It might contain partial data.', }); + interface TimeseriesChartProps extends TimeseriesChartWithContextProps { comparisonEnabled: boolean; offset?: string; timeZone: string; annotations?: Array>; + settings?: Partial; } export function TimeseriesChart({ id, @@ -68,6 +71,7 @@ export function TimeseriesChart({ offset, timeZone, annotations, + settings, }: TimeseriesChartProps) { const history = useHistory(); const { chartRef, updatePointerEvent } = useChartPointerEventContext(); @@ -186,6 +190,7 @@ export function TimeseriesChart({ } }} locale={i18n.getLocale()} + {...settings} /> { environment: 'test', }, agent: { name: ['nodejs'] }, - data_stream: { type: ['metrics', 'logs'] }, + source_data_stream: { type: ['metrics', 'logs'] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -64,7 +64,7 @@ describe('mergeEntities', () => { environment: 'env-service-1', }, agent: { name: ['nodejs'] }, - data_stream: { type: ['foo'] }, + source_data_stream: { type: ['foo'] }, entity: { firstSeenTimestamp: '2024-03-05T10:34:40.810Z', lastSeenTimestamp: '2024-03-05T10:34:40.810Z', @@ -85,7 +85,7 @@ describe('mergeEntities', () => { environment: 'env-service-2', }, agent: { name: ['nodejs'] }, - data_stream: { type: ['bar'] }, + source_data_stream: { type: ['bar'] }, entity: { firstSeenTimestamp: '2024-03-05T10:34:40.810Z', lastSeenTimestamp: '2024-03-05T10:34:40.810Z', @@ -106,7 +106,7 @@ describe('mergeEntities', () => { environment: 'env-service-3', }, agent: { name: ['java'] }, - data_stream: { type: ['baz'] }, + source_data_stream: { type: ['baz'] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -127,7 +127,7 @@ describe('mergeEntities', () => { environment: 'env-service-4', }, agent: { name: ['java'] }, - data_stream: { type: ['baz'] }, + source_data_stream: { type: ['baz'] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -204,7 +204,7 @@ describe('mergeEntities', () => { environment: 'test', }, agent: { name: ['nodejs'] }, - data_stream: { type: ['metrics', 'logs'] }, + source_data_stream: { type: ['metrics', 'logs'] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -225,7 +225,7 @@ describe('mergeEntities', () => { environment: 'test', }, agent: { name: ['nodejs'] }, - data_stream: { type: ['metrics', 'logs'] }, + source_data_stream: { type: ['metrics', 'logs'] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -246,7 +246,7 @@ describe('mergeEntities', () => { environment: 'prod', }, agent: { name: ['nodejs'] }, - data_stream: { type: ['foo'] }, + source_data_stream: { type: ['foo'] }, entity: { firstSeenTimestamp: '2024-23-05T10:34:40.810Z', lastSeenTimestamp: '2024-23-05T10:34:40.810Z', @@ -305,7 +305,7 @@ describe('mergeEntities', () => { environment: undefined, }, agent: { name: ['nodejs'] }, - data_stream: { type: [] }, + source_data_stream: { type: [] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -348,7 +348,7 @@ describe('mergeEntities', () => { name: 'service-1', }, agent: { name: ['nodejs'] }, - data_stream: { type: [] }, + source_data_stream: { type: [] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -368,7 +368,7 @@ describe('mergeEntities', () => { name: 'service-1', }, agent: { name: ['nodejs'] }, - data_stream: { type: [] }, + source_data_stream: { type: [] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -420,7 +420,7 @@ describe('mergeEntities', () => { name: 'service-1', }, agent: { name: ['nodejs'] }, - data_stream: { type: [] }, + source_data_stream: { type: [] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -463,7 +463,7 @@ describe('mergeEntities', () => { name: 'service-1', }, agent: { name: ['nodejs'] }, - data_stream: { type: [] }, + source_data_stream: { type: [] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -483,7 +483,7 @@ describe('mergeEntities', () => { name: 'service-1', }, agent: { name: ['nodejs'] }, - data_stream: { type: [] }, + source_data_stream: { type: [] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', @@ -536,7 +536,7 @@ describe('mergeEntities', () => { environment: 'test', }, agent: { name: ['nodejs'] }, - data_stream: { type: ['metrics'] }, + source_data_stream: { type: ['metrics'] }, entity: { firstSeenTimestamp: '2024-06-05T10:34:40.810Z', lastSeenTimestamp: '2024-06-05T10:34:40.810Z', diff --git a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts index b3705796329ce..4017d922d63c5 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/entities/utils/merge_entities.ts @@ -53,7 +53,7 @@ function mergeFunc(entity: EntityLatestServiceRaw, existingEntity?: MergedServic if (!existingEntity) { return { ...commonEntityFields, - dataStreamTypes: entity.data_stream.type, + dataStreamTypes: entity.source_data_stream.type, environments: compact([entity?.service.environment]), metrics: [entity.entity.metrics], hasLogMetrics, @@ -62,7 +62,7 @@ function mergeFunc(entity: EntityLatestServiceRaw, existingEntity?: MergedServic return { ...commonEntityFields, dataStreamTypes: uniq( - compact([...(existingEntity?.dataStreamTypes ?? []), ...entity.data_stream.type]) + compact([...(existingEntity?.dataStreamTypes ?? []), ...entity.source_data_stream.type]) ), environments: uniq(compact([...existingEntity?.environments, entity?.service.environment])), metrics: [...existingEntity?.metrics, entity.entity.metrics], diff --git a/x-pack/plugins/observability_solution/apm/tsconfig.json b/x-pack/plugins/observability_solution/apm/tsconfig.json index 6f3ff13a2af3e..9f6a5e4f57f40 100644 --- a/x-pack/plugins/observability_solution/apm/tsconfig.json +++ b/x-pack/plugins/observability_solution/apm/tsconfig.json @@ -128,6 +128,7 @@ "@kbn/aiops-log-rate-analysis", "@kbn/router-utils", "@kbn/react-hooks", + "@kbn/alerting-comparators", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/dataset_quality/README.md b/x-pack/plugins/observability_solution/dataset_quality/README.md index 356393aa5237f..45883f6964cc8 100755 --- a/x-pack/plugins/observability_solution/dataset_quality/README.md +++ b/x-pack/plugins/observability_solution/dataset_quality/README.md @@ -29,7 +29,7 @@ The deployment-agnostic API tests are located in [`x-pack/test/api_integration/d node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts # run tests -node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep=$ +node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --include ./x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/$ ``` #### Start server and run test (serverless) @@ -39,7 +39,7 @@ node scripts/functional_test_runner --config x-pack/test/api_integration/deploym node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts # run tests -node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep=$ +node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --include ./x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/$ ``` ### API integration tests diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/api_types.ts b/x-pack/plugins/observability_solution/dataset_quality/common/api_types.ts index 8bcce166b936f..bfbb2bc1cd5d1 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/api_types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/api_types.ts @@ -103,6 +103,7 @@ export const degradedFieldRt = rt.type({ y: rt.number, }) ), + indexFieldWasLastPresentIn: rt.string, }); export type DegradedField = rt.TypeOf; @@ -120,11 +121,34 @@ export const degradedFieldValuesRt = rt.type({ export type DegradedFieldValues = rt.TypeOf; -export const dataStreamSettingsRt = rt.partial({ - createdOn: rt.union([rt.null, rt.number]), // rt.null is needed because `createdOn` is not available on Serverless - integration: rt.string, - datasetUserPrivileges: datasetUserPrivilegesRt, -}); +export const degradedFieldAnalysisRt = rt.intersection([ + rt.type({ + isFieldLimitIssue: rt.boolean, + fieldCount: rt.number, + totalFieldLimit: rt.number, + }), + rt.partial({ + ignoreMalformed: rt.boolean, + nestedFieldLimit: rt.number, + fieldMapping: rt.partial({ + type: rt.string, + ignore_above: rt.number, + }), + }), +]); + +export type DegradedFieldAnalysis = rt.TypeOf; + +export const dataStreamSettingsRt = rt.intersection([ + rt.type({ + lastBackingIndexName: rt.string, + }), + rt.partial({ + createdOn: rt.union([rt.null, rt.number]), // rt.null is needed because `createdOn` is not available on Serverless + integration: rt.string, + datasetUserPrivileges: datasetUserPrivilegesRt, + }), +]); export type DataStreamSettings = rt.TypeOf; diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/types.ts b/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/types.ts index 82d6d2651be56..66b7567a2b60c 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/types.ts @@ -8,3 +8,9 @@ export interface GetDataStreamIntegrationParams { integrationName: string; } + +export interface AnalyzeDegradedFieldsParams { + dataStream: string; + lastBackingIndex: string; + degradedField: string; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/es_fields/index.ts b/x-pack/plugins/observability_solution/dataset_quality/common/es_fields/index.ts index 1e67fb1c68f81..6da01c815c79e 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/es_fields/index.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/es_fields/index.ts @@ -7,6 +7,7 @@ export const _IGNORED = '_ignored'; export const TIMESTAMP = '@timestamp'; +export const INDEX = '_index'; export const DATA_STREAM_DATASET = 'data_stream.dataset'; export const DATA_STREAM_NAMESPACE = 'data_stream.namespace'; diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/translations.ts b/x-pack/plugins/observability_solution/dataset_quality/common/translations.ts index cdaacd2f43cff..e5b660b31de10 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/translations.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/translations.ts @@ -50,6 +50,13 @@ export const openInLogsExplorerText = i18n.translate( } ); +export const logsExplorerAriaText = i18n.translate( + 'xpack.datasetQuality.details.logsExplorerAriaText', + { + defaultMessage: 'Logs Explorer', + } +); + export const openInDiscoverText = i18n.translate( 'xpack.datasetQuality.details.openInDiscoverText', { @@ -57,6 +64,10 @@ export const openInDiscoverText = i18n.translate( } ); +export const discoverAriaText = i18n.translate('xpack.datasetQuality.details.discoverAriaText', { + defaultMessage: 'Discover', +}); + export const flyoutDatasetDetailsText = i18n.translate( 'xpack.datasetQuality.flyoutDatasetDetailsText', { @@ -329,6 +340,21 @@ export const overviewDegradedFieldsSectionTitle = i18n.translate( } ); +export const overviewDegradedFieldToggleSwitch = i18n.translate( + 'xpack.datasetQuality.details.degradedFieldToggleSwitch', + { + defaultMessage: 'Current quality issues only', + } +); + +export const overviewDegradedFieldToggleSwitchTooltip = i18n.translate( + 'xpack.datasetQuality.details.degradedFieldToggleSwitchTooltip', + { + defaultMessage: + 'Enable to only show issues detected in the most recent version of the data set. Disable to show all issues detected within the configured time range.', + } +); + export const overviewDegradedFieldsSectionTitleTooltip = i18n.translate( 'xpack.datasetQuality.details.degradedFieldsSectionTooltip', { @@ -402,3 +428,75 @@ export const fieldIgnoredText = i18n.translate( defaultMessage: 'field ignored', } ); + +export const degradedFieldPotentialCauseColumnName = i18n.translate( + 'xpack.datasetQuality.details.degradedField.potentialCause', + { + defaultMessage: 'Potential cause', + } +); + +export const degradedFieldCurrentFieldLimitColumnName = i18n.translate( + 'xpack.datasetQuality.details.degradedField.currentFieldLimit', + { + defaultMessage: 'Field limit', + } +); + +export const degradedFieldMaximumCharacterLimitColumnName = i18n.translate( + 'xpack.datasetQuality.details.degradedField.maximumCharacterLimit', + { + defaultMessage: 'Maximum character length', + } +); + +export const degradedFieldCauseFieldLimitExceeded = i18n.translate( + 'xpack.datasetQuality.details.degradedField.cause.fieldLimitExceeded', + { + defaultMessage: 'field limit exceeded', + } +); + +export const degradedFieldCauseFieldLimitExceededTooltip = i18n.translate( + 'xpack.datasetQuality.details.degradedField.cause.fieldLimitExceededTooltip', + { + defaultMessage: 'The number of fields in this index has exceeded the maximum allowed limit.', + } +); + +export const degradedFieldCauseFieldIgnored = i18n.translate( + 'xpack.datasetQuality.details.degradedField.cause.fieldIgnored', + { + defaultMessage: 'field character limit exceeded', + } +); + +export const degradedFieldCauseFieldIgnoredTooltip = i18n.translate( + 'xpack.datasetQuality.details.degradedField.cause.fieldIgnoredTooltip', + { + defaultMessage: + 'One or more values for this field exceeded the maximum allowed character length. Characters above will be ignored.', + } +); + +export const degradedFieldCauseFieldMalformed = i18n.translate( + 'xpack.datasetQuality.details.degradedField.cause.fieldMalformed', + { + defaultMessage: 'field malformed', + } +); + +export const degradedFieldCauseFieldMalformedTooltip = i18n.translate( + 'xpack.datasetQuality.details.degradedField.cause.fieldMalformedTooltip', + { + defaultMessage: 'Data type for the field not set correctly.', + } +); + +export const degradedFieldMessageIssueDoesNotExistInLatestIndex = i18n.translate( + 'xpack.datasetQuality.details.degradedField.message.issueDoesNotExistInLatestIndex', + { + defaultMessage: + 'This issue was detected in an older version of the dataset, but not in the most recent version.', + } +); diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/dataset_quality_details.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/dataset_quality_details.tsx index d818ffe9aaf1b..59a1ae3d39d62 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/dataset_quality_details.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/dataset_quality_details.tsx @@ -18,7 +18,7 @@ const DegradedFieldFlyout = dynamic(() => import('./degraded_field_flyout')); // Allow for lazy loading // eslint-disable-next-line import/no-default-export export default function DatasetQualityDetails() { - const { isIndexNotFoundError, dataStream, expandedDegradedField } = + const { isIndexNotFoundError, dataStream, isDegradedFieldFlyoutOpen } = useDatasetQualityDetailsState(); const { startTracking } = useDatasetDetailsTelemetry(); @@ -38,7 +38,7 @@ export default function DatasetQualityDetails() {
- {expandedDegradedField && } + {isDegradedFieldFlyoutOpen && } ); } diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/degraded_field_flyout/field_info.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/degraded_field_flyout/field_info.tsx index 5e6756be96630..1e6bda781d733 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/degraded_field_flyout/field_info.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/degraded_field_flyout/field_info.tsx @@ -5,16 +5,16 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React from 'react'; import { EuiBadge, EuiBadgeGroup, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, - EuiSkeletonRectangle, EuiTextColor, EuiTitle, + EuiToolTip, formatNumber, } from '@elastic/eui'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; @@ -22,41 +22,42 @@ import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import { NUMBER_FORMAT } from '../../../../common/constants'; import { countColumnName, + degradedFieldCurrentFieldLimitColumnName, + degradedFieldMaximumCharacterLimitColumnName, + degradedFieldPotentialCauseColumnName, degradedFieldValuesColumnName, lastOccurrenceColumnName, } from '../../../../common/translations'; import { useDegradedFields } from '../../../hooks'; import { SparkPlot } from '../../common/spark_plot'; +import { DegradedField } from '../../../../common/api_types'; -export const DegradedFieldInfo = () => { +export const DegradedFieldInfo = ({ fieldList }: { fieldList?: DegradedField }) => { const { - renderedItems, fieldFormats, - expandedDegradedField, degradedFieldValues, isDegradedFieldsLoading, - isDegradedFieldsValueLoading, + isAnalysisInProgress, + degradedFieldAnalysisResult, + degradedFieldAnalysis, } = useDegradedFields(); const dateFormatter = fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.DATE, [ ES_FIELD_TYPES.DATE, ]); - const fieldList = useMemo(() => { - return renderedItems.find((item) => { - return item.name === expandedDegradedField; - }); - }, [renderedItems, expandedDegradedField]); - return ( - + {countColumnName} - + { - + {lastOccurrenceColumnName} - + {dateFormatter.convert(fieldList?.lastOccurrence)} - - + + + - {degradedFieldValuesColumnName} + {degradedFieldPotentialCauseColumnName} - - - {degradedFieldValues?.values.map((value) => ( - - - {value} - - - ))} - - +
+ + + {degradedFieldAnalysisResult?.potentialCause} + + +
+ + {!isAnalysisInProgress && degradedFieldAnalysis?.isFieldLimitIssue && ( + <> + + + + {degradedFieldCurrentFieldLimitColumnName} + + + + {degradedFieldAnalysis.totalFieldLimit} + + + + + )} + + {!isAnalysisInProgress && degradedFieldAnalysisResult?.shouldDisplayValues && ( + <> + + + + {degradedFieldMaximumCharacterLimitColumnName} + + + + {degradedFieldAnalysis?.fieldMapping?.ignore_above} + + + + + + + {degradedFieldValuesColumnName} + + + + + {degradedFieldValues?.values.map((value, idx) => ( + + + {value} + + + ))} + + + + + + )}
); }; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/degraded_field_flyout/index.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/degraded_field_flyout/index.tsx index 84d997296b8a9..189b3ceefe37c 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/degraded_field_flyout/index.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/degraded_field_flyout/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiBadge, EuiFlyout, @@ -15,22 +15,60 @@ import { EuiText, EuiTitle, useGeneratedHtmlId, + EuiTextColor, + EuiFlexGroup, + EuiButtonIcon, + EuiToolTip, } from '@elastic/eui'; -import { useDegradedFields } from '../../../hooks'; +import { NavigationSource } from '../../../services/telemetry'; import { + useDatasetDetailsRedirectLinkTelemetry, + useDatasetQualityDetailsState, + useDegradedFields, + useRedirectLink, +} from '../../../hooks'; +import { + degradedFieldMessageIssueDoesNotExistInLatestIndex, + discoverAriaText, fieldIgnoredText, + logsExplorerAriaText, + openInDiscoverText, + openInLogsExplorerText, overviewDegradedFieldsSectionTitle, } from '../../../../common/translations'; import { DegradedFieldInfo } from './field_info'; +import { _IGNORED } from '../../../../common/es_fields'; // Allow for lazy loading // eslint-disable-next-line import/no-default-export export default function DegradedFieldFlyout() { - const { closeDegradedFieldFlyout, expandedDegradedField } = useDegradedFields(); + const { closeDegradedFieldFlyout, expandedDegradedField, renderedItems } = useDegradedFields(); + const { dataStreamSettings, datasetDetails, timeRange } = useDatasetQualityDetailsState(); const pushedFlyoutTitleId = useGeneratedHtmlId({ prefix: 'pushedFlyoutTitle', }); + const fieldList = useMemo(() => { + return renderedItems.find((item) => { + return item.name === expandedDegradedField; + }); + }, [renderedItems, expandedDegradedField]); + + const isUserViewingTheIssueOnLatestBackingIndex = + dataStreamSettings?.lastBackingIndexName === fieldList?.indexFieldWasLastPresentIn; + + const { sendTelemetry } = useDatasetDetailsRedirectLinkTelemetry({ + query: { language: 'kuery', query: `${_IGNORED}: ${expandedDegradedField}` }, + navigationSource: NavigationSource.DegradedFieldFlyoutHeader, + }); + + const redirectLinkProps = useRedirectLink({ + dataStreamStat: datasetDetails, + timeRangeConfig: timeRange, + query: { language: 'kuery', query: `${_IGNORED}: ${expandedDegradedField}` }, + sendTelemetry, + }); + return ( {overviewDegradedFieldsSectionTitle} - - - {expandedDegradedField} {fieldIgnoredText} - - + + + + {expandedDegradedField} {fieldIgnoredText} + + + + + + + {!isUserViewingTheIssueOnLatestBackingIndex && ( + <> + + + {degradedFieldMessageIssueDoesNotExistInLatestIndex} + + + )} - + ); diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/degraded_fields.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/degraded_fields.tsx index 01956405074e3..b33bd11dbe3a6 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/degraded_fields.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/degraded_fields.tsx @@ -15,10 +15,13 @@ import { useGeneratedHtmlId, EuiBadge, EuiBetaBadge, + EuiSwitch, } from '@elastic/eui'; import { overviewDegradedFieldsSectionTitle, overviewDegradedFieldsSectionTitleTooltip, + overviewDegradedFieldToggleSwitch, + overviewDegradedFieldToggleSwitchTooltip, overviewQualityIssuesAccordionTechPreviewBadge, } from '../../../../../common/translations'; import { DegradedFieldTable } from './table'; @@ -28,8 +31,24 @@ export function DegradedFields() { const accordionId = useGeneratedHtmlId({ prefix: overviewDegradedFieldsSectionTitle, }); + const toggleTextSwitchId = useGeneratedHtmlId({ prefix: 'toggleTextSwitch' }); - const { totalItemCount } = useDegradedFields(); + const { totalItemCount, toggleCurrentQualityIssues, showCurrentQualityIssues } = + useDegradedFields(); + + const latestBackingIndexToggle = ( + <> + + + + ); const accordionTitle = ( @@ -58,6 +77,7 @@ export function DegradedFields() { buttonContent={accordionTitle} paddingSize="none" initialIsOpen={true} + extraAction={latestBackingIndexToggle} data-test-subj="datasetQualityDetailsOverviewDocumentTrends" > diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/index.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/index.tsx index a0875f1367705..05de567a6dab7 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/index.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/index.tsx @@ -6,7 +6,6 @@ */ import React, { useCallback, useEffect, useState } from 'react'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiAccordion, @@ -27,6 +26,9 @@ import type { DataViewField } from '@kbn/data-views-plugin/common'; import { css } from '@emotion/react'; import { UnifiedBreakdownFieldSelector } from '@kbn/unified-histogram-plugin/public'; import { + discoverAriaText, + logsExplorerAriaText, + openInDiscoverText, openInLogsExplorerText, overviewDegradedDocsText, } from '../../../../../../common/translations'; @@ -130,14 +132,25 @@ export default function DegradedDocs({ lastReloadTime }: { lastReloadTime: numbe onBreakdownFieldChange={breakdown.onChange} /> - + + Pick< + WithDefaultControllerState, + 'timeRange' | 'breakdownField' | 'expandedDegradedField' | 'showCurrentQualityIssues' + > > & { dataStream: string; } & { diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_details_state.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_details_state.ts index c24e78911b941..edd16652374a1 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_details_state.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_details_state.ts @@ -51,7 +51,7 @@ export const useDatasetQualityDetailsState = () => { ); const dataStreamSettings = useSelector(service, (state) => - state.matches('initializing.dataStreamSettings.initializeIntegrations') + state.matches('initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields') ? state.context.dataStreamSettings : undefined ); @@ -59,14 +59,14 @@ export const useDatasetQualityDetailsState = () => { const integrationDetails = { integration: useSelector(service, (state) => state.matches( - 'initializing.dataStreamSettings.initializeIntegrations.integrationDetails.done' + 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDetails.done' ) ? state.context.integration : undefined ), dashboard: useSelector(service, (state) => state.matches( - 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.done' + 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDashboards.done' ) ? state.context.integrationDashboards : undefined @@ -77,7 +77,7 @@ export const useDatasetQualityDetailsState = () => { service, (state) => !state.matches( - 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.unauthorized' + 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDashboards.unauthorized' ) ); @@ -103,18 +103,24 @@ export const useDatasetQualityDetailsState = () => { const loadingState = useSelector(service, (state) => ({ nonAggregatableDatasetLoading: state.matches('initializing.nonAggregatableDataset.fetching'), dataStreamDetailsLoading: state.matches('initializing.dataStreamDetails.fetching'), - dataStreamSettingsLoading: state.matches('initializing.dataStreamSettings.fetching'), + dataStreamSettingsLoading: state.matches( + 'initializing.dataStreamSettings.fetchingDataStreamSettings' + ), integrationDetailsLoadings: state.matches( - 'initializing.dataStreamSettings.initializeIntegrations.integrationDetails.fetching' + 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDetails.fetching' ), integrationDetailsLoaded: state.matches( - 'initializing.dataStreamSettings.initializeIntegrations.integrationDetails.done' + 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDetails.done' ), integrationDashboardsLoading: state.matches( - 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.fetching' + 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDashboards.fetching' ), })); + const isDegradedFieldFlyoutOpen = useSelector(service, (state) => + state.matches('initializing.degradedFieldFlyout.open') + ); + const updateTimeRange = useCallback( ({ start, end, refreshInterval }: OnRefreshProps) => { service.send({ @@ -150,5 +156,6 @@ export const useDatasetQualityDetailsState = () => { canUserAccessDashboards, canUserViewIntegrations, expandedDegradedField, + isDegradedFieldFlyoutOpen, }; }; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_fields.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_fields.ts index 6d52048715621..78ad0e53dd5e2 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_fields.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_fields.ts @@ -15,6 +15,14 @@ import { } from '../../common/constants'; import { useKibanaContextForPlugin } from '../utils'; import { useDatasetQualityDetailsState } from './use_dataset_quality_details_state'; +import { + degradedFieldCauseFieldIgnored, + degradedFieldCauseFieldIgnoredTooltip, + degradedFieldCauseFieldLimitExceeded, + degradedFieldCauseFieldLimitExceededTooltip, + degradedFieldCauseFieldMalformed, + degradedFieldCauseFieldMalformedTooltip, +} from '../../common/translations'; export type DegradedFieldSortField = keyof DegradedField; @@ -24,7 +32,10 @@ export function useDegradedFields() { services: { fieldFormats }, } = useKibanaContextForPlugin(); - const { degradedFields, expandedDegradedField } = useSelector(service, (state) => state.context); + const { degradedFields, expandedDegradedField, showCurrentQualityIssues } = useSelector( + service, + (state) => state.context + ); const { data, table } = degradedFields ?? {}; const { page, rowsPerPage, sort } = table; @@ -62,8 +73,14 @@ export function useDegradedFields() { return sortedItems.slice(page * rowsPerPage, (page + 1) * rowsPerPage); }, [data, sort.field, sort.direction, page, rowsPerPage]); + const expandedRenderedItem = useMemo(() => { + return renderedItems.find((item) => item.name === expandedDegradedField); + }, [expandedDegradedField, renderedItems]); + const isDegradedFieldsLoading = useSelector(service, (state) => - state.matches('initializing.dataStreamDegradedFields.fetching') + state.matches( + 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.dataStreamDegradedFields.fetching' + ) ); const closeDegradedFieldFlyout = useCallback( @@ -82,14 +99,71 @@ export function useDegradedFields() { [expandedDegradedField, service] ); + const toggleCurrentQualityIssues = useCallback(() => { + service.send('TOGGLE_CURRENT_QUALITY_ISSUES'); + }, [service]); + const degradedFieldValues = useSelector(service, (state) => - state.matches('initializing.initializeFixItFlow.ignoredValues.done') + state.matches('initializing.degradedFieldFlyout.open.ignoredValues.done') ? state.context.degradedFieldValues : undefined ); + const degradedFieldAnalysis = useSelector(service, (state) => + state.matches('initializing.degradedFieldFlyout.open.analyze.done') + ? state.context.degradedFieldAnalysis + : undefined + ); + + // This piece only cater field limit issue at the moment. + // In future this will cater the other 2 reasons as well + const degradedFieldAnalysisResult = useMemo(() => { + if (!degradedFieldAnalysis) { + return undefined; + } + + // 1st check if it's a field limit issue + if (degradedFieldAnalysis.isFieldLimitIssue) { + return { + potentialCause: degradedFieldCauseFieldLimitExceeded, + tooltipContent: degradedFieldCauseFieldLimitExceededTooltip, + shouldDisplayMitigation: true, + shouldDisplayValues: false, + }; + } + + // 2nd check if it's a ignored above issue + const fieldMapping = degradedFieldAnalysis.fieldMapping; + + if (fieldMapping && fieldMapping?.type === 'keyword' && fieldMapping?.ignore_above) { + const isAnyValueExceedingIgnoreAbove = degradedFieldValues?.values.some( + (value) => value.length > fieldMapping.ignore_above! + ); + if (isAnyValueExceedingIgnoreAbove) { + return { + potentialCause: degradedFieldCauseFieldIgnored, + tooltipContent: degradedFieldCauseFieldIgnoredTooltip, + shouldDisplayMitigation: false, + shouldDisplayValues: true, + }; + } + } + + // 3rd check if its a ignore_malformed issue. There is no check, at the moment. + return { + potentialCause: degradedFieldCauseFieldMalformed, + tooltipContent: degradedFieldCauseFieldMalformedTooltip, + shouldDisplayMitigation: false, + shouldDisplayValues: false, + }; + }, [degradedFieldAnalysis, degradedFieldValues]); + const isDegradedFieldsValueLoading = useSelector(service, (state) => { - return !state.matches('initializing.initializeFixItFlow.ignoredValues.done'); + return state.matches('initializing.degradedFieldFlyout.open.ignoredValues.fetching'); + }); + + const isAnalysisInProgress = useSelector(service, (state) => { + return state.matches('initializing.degradedFieldFlyout.open.analyze.fetching'); }); return { @@ -105,5 +179,11 @@ export function useDegradedFields() { closeDegradedFieldFlyout, degradedFieldValues, isDegradedFieldsValueLoading, + isAnalysisInProgress, + degradedFieldAnalysis, + degradedFieldAnalysisResult, + toggleCurrentQualityIssues, + showCurrentQualityIssues, + expandedRenderedItem, }; } diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_stream_details/data_stream_details_client.ts b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_stream_details/data_stream_details_client.ts index 9572b4419cbe7..9175d06e105b4 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_stream_details/data_stream_details_client.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_stream_details/data_stream_details_client.ts @@ -8,6 +8,8 @@ import { HttpStart } from '@kbn/core/public'; import { decodeOrThrow } from '@kbn/io-ts-utils'; import { + DegradedFieldAnalysis, + degradedFieldAnalysisRt, DegradedFieldValues, degradedFieldValuesRt, getDataStreamDegradedFieldsResponseRt, @@ -32,7 +34,10 @@ import { } from '../../../common/data_streams_stats'; import { IDataStreamDetailsClient } from './types'; import { Integration } from '../../../common/data_streams_stats/integration'; -import { GetDataStreamIntegrationParams } from '../../../common/data_stream_details/types'; +import { + AnalyzeDegradedFieldsParams, + GetDataStreamIntegrationParams, +} from '../../../common/data_stream_details/types'; import { DatasetQualityError } from '../../../common/errors'; export class DataStreamDetailsClient implements IDataStreamDetailsClient { @@ -167,4 +172,28 @@ export class DataStreamDetailsClient implements IDataStreamDetailsClient { if (integration) return Integration.create(integration); } + + public async analyzeDegradedField({ + dataStream, + degradedField, + lastBackingIndex, + }: AnalyzeDegradedFieldsParams): Promise { + const response = await this.http + .get( + `/internal/dataset_quality/data_streams/${dataStream}/degraded_field/${degradedField}/analyze`, + { query: { lastBackingIndex } } + ) + .catch((error) => { + throw new DatasetQualityError( + `Failed to analyze degraded field: ${degradedField} for datastream: ${dataStream}`, + error + ); + }); + + return decodeOrThrow( + degradedFieldAnalysisRt, + (message: string) => + new DatasetQualityError(`Failed to decode the analysis response: ${message}`) + )(response); + } } diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_stream_details/types.ts b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_stream_details/types.ts index 51fed7525bfc9..a2f7db99e5af1 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_stream_details/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_stream_details/types.ts @@ -17,8 +17,11 @@ import { DegradedFieldResponse, GetDataStreamDegradedFieldValuesPathParams, } from '../../../common/data_streams_stats'; -import { GetDataStreamIntegrationParams } from '../../../common/data_stream_details/types'; -import { Dashboard, DegradedFieldValues } from '../../../common/api_types'; +import { + AnalyzeDegradedFieldsParams, + GetDataStreamIntegrationParams, +} from '../../../common/data_stream_details/types'; +import { Dashboard, DegradedFieldAnalysis, DegradedFieldValues } from '../../../common/api_types'; export type DataStreamDetailsServiceSetup = void; @@ -43,4 +46,5 @@ export interface IDataStreamDetailsClient { getDataStreamIntegration( params: GetDataStreamIntegrationParams ): Promise; + analyzeDegradedField(params: AnalyzeDegradedFieldsParams): Promise; } diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/services/telemetry/types.ts b/x-pack/plugins/observability_solution/dataset_quality/public/services/telemetry/types.ts index f1a41ffc666cc..a0c86f6a5bd94 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/services/telemetry/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/services/telemetry/types.ts @@ -37,6 +37,7 @@ export enum NavigationSource { Trend = 'trend', Table = 'table', ActionMenu = 'action_menu', + DegradedFieldFlyoutHeader = 'degraded_field_flyout_header', } export interface WithTrackingId { diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/defaults.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/defaults.ts index 024e49a9b83f4..26a51014b3abb 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/defaults.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/defaults.ts @@ -29,4 +29,5 @@ export const DEFAULT_CONTEXT: DefaultDatasetQualityDetailsContext = { ...DEFAULT_TIME_RANGE, refresh: DEFAULT_DATEPICKER_REFRESH, }, + showCurrentQualityIssues: false, }; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/state_machine.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/state_machine.ts index 86dbc879093f2..352aff140c275 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/state_machine.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/state_machine.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { assign, createMachine, DoneInvokeEvent, InterpreterFrom } from 'xstate'; +import { assign, createMachine, DoneInvokeEvent, InterpreterFrom, raise } from 'xstate'; import { getDateISORange } from '@kbn/timerange'; import type { IToasts } from '@kbn/core-notifications-browser'; import { @@ -21,6 +21,7 @@ import { Dashboard, DataStreamDetails, DataStreamSettings, + DegradedFieldAnalysis, DegradedFieldResponse, DegradedFieldValues, NonAggregatableDatasets, @@ -47,13 +48,8 @@ export const createPureDatasetQualityDetailsControllerStateMachine = ( id: 'DatasetQualityDetailsController', context: initialContext, predictableActionArguments: true, - initial: 'uninitialized', + initial: 'initializing', states: { - uninitialized: { - always: { - target: 'initializing', - }, - }, initializing: { type: 'parallel', states: { @@ -145,58 +141,14 @@ export const createPureDatasetQualityDetailsControllerStateMachine = ( done: {}, }, }, - dataStreamDegradedFields: { - initial: 'fetching', - states: { - fetching: { - invoke: { - src: 'loadDegradedFields', - onDone: { - target: 'done', - actions: ['storeDegradedFields'], - }, - onError: [ - { - target: '#DatasetQualityDetailsController.indexNotFound', - cond: 'isIndexNotFoundError', - }, - { - target: 'done', - }, - ], - }, - }, - done: { - on: { - UPDATE_TIME_RANGE: { - target: 'fetching', - actions: ['resetDegradedFieldPageAndRowsPerPage'], - }, - UPDATE_DEGRADED_FIELDS_TABLE_CRITERIA: { - target: 'done', - actions: ['storeDegradedFieldTableOptions'], - }, - OPEN_DEGRADED_FIELD_FLYOUT: { - target: - '#DatasetQualityDetailsController.initializing.initializeFixItFlow.ignoredValues', - actions: ['storeExpandedDegradedField'], - }, - CLOSE_DEGRADED_FIELD_FLYOUT: { - target: 'done', - actions: ['storeExpandedDegradedField'], - }, - }, - }, - }, - }, dataStreamSettings: { - initial: 'fetching', + initial: 'fetchingDataStreamSettings', states: { - fetching: { + fetchingDataStreamSettings: { invoke: { src: 'loadDataStreamSettings', onDone: { - target: 'initializeIntegrations', + target: 'loadingIntegrationsAndDegradedFields', actions: ['storeDataStreamSettings'], }, onError: [ @@ -211,9 +163,53 @@ export const createPureDatasetQualityDetailsControllerStateMachine = ( ], }, }, - initializeIntegrations: { + loadingIntegrationsAndDegradedFields: { type: 'parallel', states: { + dataStreamDegradedFields: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'loadDegradedFields', + onDone: { + target: 'done', + actions: ['storeDegradedFields', 'raiseDegradedFieldsLoaded'], + }, + onError: [ + { + target: '#DatasetQualityDetailsController.indexNotFound', + cond: 'isIndexNotFoundError', + }, + { + target: 'done', + }, + ], + }, + }, + done: { + on: { + UPDATE_TIME_RANGE: { + target: 'fetching', + actions: ['resetDegradedFieldPageAndRowsPerPage'], + }, + UPDATE_DEGRADED_FIELDS_TABLE_CRITERIA: { + target: 'done', + actions: ['storeDegradedFieldTableOptions'], + }, + OPEN_DEGRADED_FIELD_FLYOUT: { + target: + '#DatasetQualityDetailsController.initializing.degradedFieldFlyout.open', + actions: ['storeExpandedDegradedField'], + }, + TOGGLE_CURRENT_QUALITY_ISSUES: { + target: 'fetching', + actions: ['toggleCurrentQualityIssues'], + }, + }, + }, + }, + }, integrationDetails: { initial: 'fetching', states: { @@ -230,9 +226,7 @@ export const createPureDatasetQualityDetailsControllerStateMachine = ( }, }, }, - done: { - type: 'final', - }, + done: {}, }, }, integrationDashboards: { @@ -257,61 +251,115 @@ export const createPureDatasetQualityDetailsControllerStateMachine = ( ], }, }, - done: { - type: 'final', - }, + done: {}, unauthorized: { type: 'final', }, }, }, }, - }, - done: { - on: { - UPDATE_TIME_RANGE: { - target: 'fetching', - actions: ['resetDegradedFieldPageAndRowsPerPage'], - }, + onDone: { + target: 'done', }, }, + done: {}, + }, + on: { + UPDATE_TIME_RANGE: { + target: '.fetchingDataStreamSettings', + }, }, }, - initializeFixItFlow: { - initial: 'closed', - type: 'parallel', + degradedFieldFlyout: { + initial: 'pending', states: { - ignoredValues: { - initial: 'fetching', + pending: { + always: [ + { + target: 'closed', + cond: 'hasNoDegradedFieldsSelected', + }, + ], + }, + open: { + type: 'parallel', states: { - fetching: { - invoke: { - src: 'loadDegradedFieldValues', - onDone: { - target: 'done', - actions: ['storeDegradedFieldValues'], - }, - onError: [ - { - target: '#DatasetQualityDetailsController.indexNotFound', - cond: 'isIndexNotFoundError', - }, - { - target: 'done', + ignoredValues: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'loadDegradedFieldValues', + onDone: { + target: 'done', + actions: ['storeDegradedFieldValues'], + }, + onError: [ + { + target: '#DatasetQualityDetailsController.indexNotFound', + cond: 'isIndexNotFoundError', + }, + { + target: 'done', + }, + ], }, - ], + }, + done: {}, }, }, - done: { - on: { - UPDATE_TIME_RANGE: { - target: 'fetching', + analyze: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'analyzeDegradedField', + onDone: { + target: 'done', + actions: ['storeDegradedFieldAnalysis'], + }, + onError: { + target: 'done', + }, + }, }, + done: {}, }, }, }, + on: { + CLOSE_DEGRADED_FIELD_FLYOUT: { + target: 'closed', + actions: ['storeExpandedDegradedField'], + }, + UPDATE_TIME_RANGE: { + target: + '#DatasetQualityDetailsController.initializing.degradedFieldFlyout.open', + }, + }, + }, + closed: { + on: { + OPEN_DEGRADED_FIELD_FLYOUT: { + target: + '#DatasetQualityDetailsController.initializing.degradedFieldFlyout.open', + actions: ['storeExpandedDegradedField'], + }, + }, }, }, + on: { + DEGRADED_FIELDS_LOADED: [ + { + target: '.open', + cond: 'shouldOpenFlyout', + }, + { + target: '.closed', + actions: ['storeExpandedDegradedField'], + }, + ], + }, }, }, }, @@ -370,6 +418,13 @@ export const createPureDatasetQualityDetailsControllerStateMachine = ( } : {}; }), + storeDegradedFieldAnalysis: assign((_, event: DoneInvokeEvent) => { + return 'data' in event + ? { + degradedFieldAnalysis: event.data, + } + : {}; + }), storeDegradedFieldTableOptions: assign((context, event) => { return 'degraded_field_criteria' in event ? { @@ -380,11 +435,17 @@ export const createPureDatasetQualityDetailsControllerStateMachine = ( } : {}; }), - storeExpandedDegradedField: assign((context, event) => { + storeExpandedDegradedField: assign((_, event) => { return { expandedDegradedField: 'fieldName' in event ? event.fieldName : undefined, }; }), + toggleCurrentQualityIssues: assign((context) => { + return { + showCurrentQualityIssues: !context.showCurrentQualityIssues, + }; + }), + raiseDegradedFieldsLoaded: raise('DEGRADED_FIELDS_LOADED'), resetDegradedFieldPageAndRowsPerPage: assign((context, _event) => ({ degradedFields: { ...context.degradedFields, @@ -442,6 +503,19 @@ export const createPureDatasetQualityDetailsControllerStateMachine = ( false ); }, + shouldOpenFlyout: (context) => { + return ( + Boolean(context.expandedDegradedField) && + Boolean( + context.degradedFields.data?.some( + (field) => field.name === context.expandedDegradedField + ) + ) + ); + }, + hasNoDegradedFieldsSelected: (context) => { + return !Boolean(context.expandedDegradedField); + }, }, } ); @@ -524,18 +598,46 @@ export const createDatasetQualityDetailsControllerStateMachine = ({ loadDegradedFields: (context) => { const { startDate: start, endDate: end } = getDateISORange(context.timeRange); - return dataStreamDetailsClient.getDataStreamDegradedFields({ - dataStream: context.dataStream, - start, - end, - }); + if (!context?.isNonAggregatable) { + return dataStreamDetailsClient.getDataStreamDegradedFields({ + dataStream: + context.showCurrentQualityIssues && + 'dataStreamSettings' in context && + context.dataStreamSettings + ? context.dataStreamSettings.lastBackingIndexName + : context.dataStream, + start, + end, + }); + } + + return Promise.resolve(); }, loadDegradedFieldValues: (context) => { - return dataStreamDetailsClient.getDataStreamDegradedFieldValues({ - dataStream: context.dataStream, - degradedField: context.expandedDegradedField!, - }); + if ('expandedDegradedField' in context && context.expandedDegradedField) { + return dataStreamDetailsClient.getDataStreamDegradedFieldValues({ + dataStream: context.dataStream, + degradedField: context.expandedDegradedField, + }); + } + return Promise.resolve(); + }, + analyzeDegradedField: (context) => { + if (context?.degradedFields?.data?.length) { + const selectedDegradedField = context.degradedFields.data.find( + (field) => field.name === context.expandedDegradedField + ); + + if (selectedDegradedField) { + return dataStreamDetailsClient.analyzeDegradedField({ + dataStream: context.dataStream, + degradedField: context.expandedDegradedField!, + lastBackingIndex: selectedDegradedField.indexFieldWasLastPresentIn, + }); + } + } + return Promise.resolve(); }, loadDataStreamSettings: (context) => { return dataStreamDetailsClient.getDataStreamSettings({ diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/types.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/types.ts index 3d57987743d68..cdf3bfa579e55 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/types.ts @@ -12,6 +12,7 @@ import { DataStreamDetails, DataStreamSettings, DegradedField, + DegradedFieldAnalysis, DegradedFieldResponse, DegradedFieldValues, NonAggregatableDatasets, @@ -40,11 +41,13 @@ export interface WithDefaultControllerState { dataStream: string; degradedFields: DegradedFieldsTableConfig; timeRange: TimeRangeConfig; + showCurrentQualityIssues: boolean; breakdownField?: string; isBreakdownFieldEcs?: boolean; isIndexNotFoundError?: boolean; integration?: Integration; expandedDegradedField?: string; + isNonAggregatable?: boolean; } export interface WithDataStreamDetails { @@ -80,24 +83,29 @@ export interface WithDegradedFieldValues { degradedFieldValues: DegradedFieldValues; } +export interface WithDegradeFieldAnalysis { + degradedFieldAnalysis: DegradedFieldAnalysis; +} + export type DefaultDatasetQualityDetailsContext = Pick< WithDefaultControllerState, - 'degradedFields' | 'timeRange' | 'isIndexNotFoundError' + 'degradedFields' | 'timeRange' | 'isIndexNotFoundError' | 'showCurrentQualityIssues' >; export type DatasetQualityDetailsControllerTypeState = | { value: | 'initializing' - | 'uninitialized' | 'initializing.nonAggregatableDataset.fetching' - | 'initializing.dataStreamDegradedFields.fetching' - | 'initializing.dataStreamSettings.fetching' + | 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.dataStreamDegradedFields.fetching' + | 'initializing.dataStreamSettings.fetchingDataStreamSettings' | 'initializing.dataStreamDetails.fetching'; context: WithDefaultControllerState; } | { - value: 'initializing.nonAggregatableDataset.done'; + value: + | 'initializing.nonAggregatableDataset.done' + | 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.dataStreamDegradedFields.fetching'; context: WithDefaultControllerState & WithNonAggregatableDatasetStatus; } | { @@ -113,29 +121,44 @@ export type DatasetQualityDetailsControllerTypeState = context: WithDefaultControllerState & WithBreakdownInEcsCheck; } | { - value: 'initializing.dataStreamDegradedFields.done'; - context: WithDefaultControllerState & WithDegradedFieldsData; + value: 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.dataStreamDegradedFields.done'; + context: WithDefaultControllerState & + WithNonAggregatableDatasetStatus & + WithDegradedFieldsData; } | { - value: 'initializing.initializeFixItFlow.ignoredValues.fetching'; + value: + | 'initializing.degradedFieldFlyout.open.ignoredValues.fetching' + | 'initializing.degradedFieldFlyout.open.analyze.fetching'; context: WithDefaultControllerState & WithDegradedFieldsData; } | { - value: 'initializing.initializeFixItFlow.ignoredValues.done'; + value: 'initializing.degradedFieldFlyout.open.ignoredValues.done'; context: WithDefaultControllerState & WithDegradedFieldsData & WithDegradedFieldValues; } + | { + value: 'initializing.degradedFieldFlyout.open.analyze.done'; + context: WithDefaultControllerState & WithDegradedFieldsData & WithDegradeFieldAnalysis; + } + | { + value: 'initializing.degradedFieldFlyout.open'; + context: WithDefaultControllerState & + WithDegradedFieldsData & + WithDegradedFieldValues & + WithDegradeFieldAnalysis; + } | { value: - | 'initializing.dataStreamSettings.initializeIntegrations' - | 'initializing.dataStreamSettings.initializeIntegrations.integrationDetails.fetching' - | 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.fetching' - | 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.unauthorized'; + | 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields' + | 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDetails.fetching' + | 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDashboards.fetching' + | 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDashboards.unauthorized'; context: WithDefaultControllerState & WithDataStreamSettings; } | { value: - | 'initializing.dataStreamSettings.initializeIntegrations.integrationDetails.done' - | 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.done'; + | 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDetails.done' + | 'initializing.dataStreamSettings.loadingIntegrationsAndDegradedFields.integrationDashboards.done'; context: WithDefaultControllerState & WithDataStreamSettings & WithIntegration; }; @@ -154,6 +177,9 @@ export type DatasetQualityDetailsControllerEvent = | { type: 'CLOSE_DEGRADED_FIELD_FLYOUT'; } + | { + type: 'DEGRADED_FIELDS_LOADED'; + } | { type: 'BREAKDOWN_FIELD_CHANGE'; breakdownField: string | undefined; @@ -170,4 +196,5 @@ export type DatasetQualityDetailsControllerEvent = | DoneInvokeEvent | DoneInvokeEvent | DoneInvokeEvent - | DoneInvokeEvent; + | DoneInvokeEvent + | DoneInvokeEvent; diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_data_stream_details/index.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_data_stream_details/index.ts index c24ac84b10772..fd117d65ac99d 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_data_stream_details/index.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_data_stream_details/index.ts @@ -36,12 +36,15 @@ export async function getDataStreamSettings({ dataStreamService.getMatchingDataStreams(esClient, dataStream), datasetQualityPrivileges.getDatasetPrivileges(esClient, dataStream), ]); + const integration = dataStreamInfo?._meta?.package?.name; + const lastBackingIndex = dataStreamInfo?.indices?.slice(-1)[0]; return { createdOn, integration, datasetUserPrivileges, + lastBackingIndexName: lastBackingIndex?.index_name, }; } diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_field_analysis/get_datastream_mappings.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_field_analysis/get_datastream_mappings.ts new file mode 100644 index 0000000000000..865110c028a26 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_field_analysis/get_datastream_mappings.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + MappingTypeMapping, + MappingProperty, + PropertyName, +} from '@elastic/elasticsearch/lib/api/types'; +import { DatasetQualityESClient } from '../../../utils/create_dataset_quality_es_client'; + +export interface DataStreamMappingResponse { + fieldCount: number; + fieldPresent: boolean; + fieldMapping?: { + type?: string; + ignore_above?: number; + }; +} + +type MappingWithProperty = MappingTypeMapping & { + properties: Record; +}; + +type MappingWithFields = MappingTypeMapping & { + fields: Record; +}; +export async function getDataStreamMapping({ + field, + datasetQualityESClient, + dataStream, + lastBackingIndex, +}: { + field: string; + datasetQualityESClient: DatasetQualityESClient; + dataStream: string; + lastBackingIndex: string; +}): Promise { + const mappings = await datasetQualityESClient.mappings({ index: dataStream }); + const properties = mappings[lastBackingIndex]?.mappings?.properties; + const { count: fieldCount, capturedMapping: mapping } = countFields(properties ?? {}, field); + const fieldPresent = mapping !== undefined; + const fieldMapping = fieldPresent + ? { + type: mapping?.type, + ignore_above: (mapping as any)?.ignore_above, + } + : undefined; + + return { + fieldCount, + fieldPresent, + fieldMapping, + }; +} + +function isNestedProperty(property: MappingProperty): property is MappingWithProperty { + return 'properties' in property && property.properties !== undefined; +} + +function isNestedField(property: MappingProperty): property is MappingWithFields { + return 'fields' in property && property.fields !== undefined; +} + +function countFields( + mappings: Record, + captureField?: string, + prefix = '' +): { count: number; capturedMapping?: any } { + let fieldCount = 0; + let capturedMapping; + + for (const field in mappings) { + if (Object.prototype.hasOwnProperty.call(mappings, field)) { + const mappingField = mappings[field]; + const currentPath = [prefix, field].filter(Boolean).join('.'); + + // Capture the value if the current path matches the captureField + if (captureField && currentPath === captureField) { + capturedMapping = mappingField; + } + + fieldCount++; // Count the current field + + // If there are properties, recursively count nested fields + if (isNestedProperty(mappingField)) { + const { count, capturedMapping: nestedCapturedValue } = countFields( + mappingField.properties, + captureField, + currentPath + ); + fieldCount += count; + if (nestedCapturedValue !== undefined) { + capturedMapping = nestedCapturedValue; + } + } + + // If there are fields, recursively count nested fields + if (isNestedField(mappingField)) { + const { count, capturedMapping: nestedCapturedValue } = countFields( + mappingField.fields, + captureField, + currentPath + ); + fieldCount += count; + if (nestedCapturedValue !== undefined) { + capturedMapping = nestedCapturedValue; + } + } + } + } + + return { count: fieldCount, capturedMapping }; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_field_analysis/get_datastream_settings.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_field_analysis/get_datastream_settings.ts new file mode 100644 index 0000000000000..433086c0b3e52 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_field_analysis/get_datastream_settings.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DatasetQualityESClient } from '../../../utils/create_dataset_quality_es_client'; +import { toBoolean } from '../../../utils/to_boolean'; + +export interface DataStreamSettingResponse { + nestedFieldLimit?: number; + totalFieldLimit: number; + ignoreDynamicBeyondLimit?: boolean; + ignoreMalformed?: boolean; +} + +const DEFAULT_FIELD_LIMIT = 1000; +const DEFAULT_NESTED_FIELD_LIMIT = 50; + +export async function getDataStreamSettings({ + datasetQualityESClient, + dataStream, + lastBackingIndex, +}: { + datasetQualityESClient: DatasetQualityESClient; + dataStream: string; + lastBackingIndex: string; +}): Promise { + const settings = await datasetQualityESClient.settings({ index: dataStream }); + const indexSettings = settings[lastBackingIndex]?.settings?.index?.mapping; + + return { + nestedFieldLimit: indexSettings?.nested_fields?.limit + ? Number(indexSettings?.nested_fields?.limit) + : DEFAULT_NESTED_FIELD_LIMIT, + totalFieldLimit: indexSettings?.total_fields?.limit + ? Number(indexSettings?.total_fields?.limit) + : DEFAULT_FIELD_LIMIT, + ignoreDynamicBeyondLimit: toBoolean(indexSettings?.total_fields?.ignore_dynamic_beyond_limit), + ignoreMalformed: toBoolean(indexSettings?.ignore_malformed), + }; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_field_analysis/index.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_field_analysis/index.ts new file mode 100644 index 0000000000000..a0e7606b475b2 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_field_analysis/index.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { DegradedFieldAnalysis } from '../../../../common/api_types'; +import { createDatasetQualityESClient } from '../../../utils'; +import { getDataStreamMapping } from './get_datastream_mappings'; +import { getDataStreamSettings } from './get_datastream_settings'; + +// TODO: The API should also in future return some analysis around the ignore_malformed check. +// As this check is expensive and steps are not very concrete, its not done for the initial iteration +export async function analyzeDegradedField({ + esClient, + dataStream, + degradedField, + lastBackingIndex, +}: { + esClient: ElasticsearchClient; + dataStream: string; + degradedField: string; + lastBackingIndex: string; +}): Promise { + const datasetQualityESClient = createDatasetQualityESClient(esClient); + + const [ + { fieldCount, fieldPresent, fieldMapping }, + { nestedFieldLimit, totalFieldLimit, ignoreDynamicBeyondLimit, ignoreMalformed }, + ] = await Promise.all([ + getDataStreamMapping({ + datasetQualityESClient, + dataStream, + field: degradedField, + lastBackingIndex, + }), + getDataStreamSettings({ datasetQualityESClient, dataStream, lastBackingIndex }), + ]); + + return { + isFieldLimitIssue: Boolean( + !fieldPresent && ignoreDynamicBeyondLimit && fieldCount === totalFieldLimit + ), + fieldCount, + fieldMapping, + totalFieldLimit, + ignoreMalformed, + nestedFieldLimit, + }; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_fields/index.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_fields/index.ts index 2949a3aa99d68..0bb0b6a695fef 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_fields/index.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/get_degraded_fields/index.ts @@ -10,7 +10,7 @@ import { rangeQuery, existsQuery } from '@kbn/observability-plugin/server'; import { DegradedFieldResponse } from '../../../../common/api_types'; import { MAX_DEGRADED_FIELDS } from '../../../../common/constants'; import { createDatasetQualityESClient } from '../../../utils'; -import { _IGNORED, TIMESTAMP } from '../../../../common/es_fields'; +import { _IGNORED, INDEX, TIMESTAMP } from '../../../../common/es_fields'; import { getFieldIntervalInSeconds } from './get_interval'; export async function getDegradedFields({ @@ -43,6 +43,15 @@ export async function getDegradedFields({ field: TIMESTAMP, }, }, + index: { + terms: { + size: 1, + field: INDEX, + order: { + _key: 'desc', + }, + }, + }, timeSeries: { date_histogram: { field: TIMESTAMP, @@ -80,6 +89,7 @@ export async function getDegradedFields({ x: timeSeriesBucket.key, y: timeSeriesBucket.doc_count, })), + indexFieldWasLastPresentIn: bucket.index.buckets[0].key as string, })) ?? [], }; } diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/routes.ts b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/routes.ts index 869e60c6bfaa3..047004d58a6a2 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/routes.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/server/routes/data_streams/routes.ts @@ -15,6 +15,7 @@ import { DegradedFieldResponse, DatasetUserPrivileges, DegradedFieldValues, + DegradedFieldAnalysis, } from '../../../common/api_types'; import { rangeRt, typeRt, typesRt } from '../../types/default_api_types'; import { createDatasetQualityServerRoute } from '../create_datasets_quality_server_route'; @@ -26,6 +27,7 @@ import { getDegradedDocsPaginated } from './get_degraded_docs'; import { getNonAggregatableDataStreams } from './get_non_aggregatable_data_streams'; import { getDegradedFields } from './get_degraded_fields'; import { getDegradedFieldValues } from './get_degraded_field_values'; +import { analyzeDegradedField } from './get_degraded_field_analysis'; import { getDataStreamsMeteringStats } from './get_data_streams_metering_stats'; const statsRoute = createDatasetQualityServerRoute({ @@ -291,6 +293,37 @@ const dataStreamDetailsRoute = createDatasetQualityServerRoute({ }, }); +const analyzeDegradedFieldRoute = createDatasetQualityServerRoute({ + endpoint: + 'GET /internal/dataset_quality/data_streams/{dataStream}/degraded_field/{degradedField}/analyze', + params: t.type({ + path: t.type({ + dataStream: t.string, + degradedField: t.string, + }), + query: t.type({ + lastBackingIndex: t.string, + }), + }), + options: { + tags: [], + }, + async handler(resources): Promise { + const { context, params } = resources; + const coreContext = await context.core; + const esClient = coreContext.elasticsearch.client.asCurrentUser; + + const degradedFieldAnalysis = await analyzeDegradedField({ + esClient, + dataStream: params.path.dataStream, + degradedField: params.path.degradedField, + lastBackingIndex: params.query.lastBackingIndex, + }); + + return degradedFieldAnalysis; + }, +}); + export const dataStreamsRouteRepository = { ...statsRoute, ...degradedDocsRoute, @@ -300,4 +333,5 @@ export const dataStreamsRouteRepository = { ...degradedFieldValuesRoute, ...dataStreamDetailsRoute, ...dataStreamSettingsRoute, + ...analyzeDegradedFieldRoute, }; diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/utils/create_dataset_quality_es_client.ts b/x-pack/plugins/observability_solution/dataset_quality/server/utils/create_dataset_quality_es_client.ts index 414c313ee373c..baa2403690fd8 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/server/utils/create_dataset_quality_es_client.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/server/utils/create_dataset_quality_es_client.ts @@ -7,7 +7,13 @@ import { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types'; import { ElasticsearchClient } from '@kbn/core/server'; -import { FieldCapsRequest, FieldCapsResponse, Indices } from '@elastic/elasticsearch/lib/api/types'; +import { + FieldCapsRequest, + FieldCapsResponse, + Indices, + IndicesGetMappingResponse, + IndicesGetSettingsResponse, +} from '@elastic/elasticsearch/lib/api/types'; type DatasetQualityESSearchParams = ESSearchRequest & { size: number; @@ -35,5 +41,11 @@ export function createDatasetQualityESClient(esClient: ElasticsearchClient) { async fieldCaps(params: FieldCapsRequest): Promise { return esClient.fieldCaps(params) as Promise; }, + async mappings(params: { index: string }): Promise { + return esClient.indices.getMapping(params); + }, + async settings(params: { index: string }): Promise { + return esClient.indices.getSettings(params); + }, }; } diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/utils/to_boolean.ts b/x-pack/plugins/observability_solution/dataset_quality/server/utils/to_boolean.ts new file mode 100644 index 0000000000000..395b2ee3ca645 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/server/utils/to_boolean.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function toBoolean(value?: string | boolean): boolean { + if (typeof value === 'string') { + return value.toLowerCase() === 'true'; + } + return Boolean(value); +} diff --git a/x-pack/plugins/observability_solution/infra/kibana.jsonc b/x-pack/plugins/observability_solution/infra/kibana.jsonc index cf73b1636d93e..0f039fb02e356 100644 --- a/x-pack/plugins/observability_solution/infra/kibana.jsonc +++ b/x-pack/plugins/observability_solution/infra/kibana.jsonc @@ -35,7 +35,8 @@ "usageCollection", "visTypeTimeseries", "apmDataAccess", - "logsDataAccess" + "logsDataAccess", + "entityManager" ], "optionalPlugins": [ "spaces", diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts b/x-pack/plugins/observability_solution/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts index c76778aa81251..61a0859670549 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts +++ b/x-pack/plugins/observability_solution/infra/public/alerting/log_threshold/components/alert_details_app_section/types.ts @@ -6,11 +6,10 @@ */ import { Rule } from '@kbn/alerting-plugin/common'; -import { AlertSummaryField, TopAlert } from '@kbn/observability-plugin/public'; +import { TopAlert } from '@kbn/observability-plugin/public'; import { PartialRuleParams } from '../../../../../common/alerting/logs/log_threshold'; export interface AlertDetailsAppSectionProps { rule: Rule; alert: TopAlert>; - setAlertSummaryFields: React.Dispatch>; } diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx index 5f8b99629eeb8..822173c5144df 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.test.tsx @@ -69,7 +69,6 @@ jest.mock('../../../hooks/use_kibana', () => ({ describe('AlertDetailsAppSection', () => { const queryClient = new QueryClient(); - const mockedSetAlertSummaryFields = jest.fn(); const renderComponent = () => { return render( @@ -77,7 +76,6 @@ describe('AlertDetailsAppSection', () => { diff --git a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx index 78d908d85ad8c..8f3e22c5b8a84 100644 --- a/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx +++ b/x-pack/plugins/observability_solution/infra/public/alerting/metric_threshold/components/alert_details_app_section.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import chroma from 'chroma-js'; -import { AlertSummaryField, RuleConditionChart, TopAlert } from '@kbn/observability-plugin/public'; +import { RuleConditionChart, TopAlert } from '@kbn/observability-plugin/public'; import { ALERT_END, ALERT_START, ALERT_EVALUATION_VALUES, ALERT_GROUP } from '@kbn/rule-data-utils'; import { Rule, RuleTypeParams } from '@kbn/alerting-plugin/common'; import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; @@ -57,10 +57,9 @@ export type MetricThresholdAlert = TopAlert; interface AppSectionProps { alert: MetricThresholdAlert; rule: MetricThresholdRule; - setAlertSummaryFields: React.Dispatch>; } -export function AlertDetailsAppSection({ alert, rule, setAlertSummaryFields }: AppSectionProps) { +export function AlertDetailsAppSection({ alert, rule }: AppSectionProps) { const { charts } = useKibanaContextForPlugin().services; const { euiTheme } = useEuiTheme(); const groups = alert.fields[ALERT_GROUP]; diff --git a/x-pack/plugins/observability_solution/infra/server/features.ts b/x-pack/plugins/observability_solution/infra/server/features.ts index 48091c9fe4b7f..b2f83967920e1 100644 --- a/x-pack/plugins/observability_solution/infra/server/features.ts +++ b/x-pack/plugins/observability_solution/infra/server/features.ts @@ -106,7 +106,7 @@ export const LOGS_FEATURE = { order: 700, category: DEFAULT_APP_CATEGORIES.observability, scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security], - app: ['infra', 'logs', 'kibana'], + app: ['infra', 'logs', 'kibana', 'observability-logs-explorer'], catalogue: ['infralogging', 'logs'], management: { insightsAndAlerting: ['triggersActions'], @@ -114,7 +114,7 @@ export const LOGS_FEATURE = { alerting: logsRuleTypes, privileges: { all: { - app: ['infra', 'logs', 'kibana'], + app: ['infra', 'logs', 'kibana', 'observability-logs-explorer'], catalogue: ['infralogging', 'logs'], api: ['infra', 'rac'], savedObject: { @@ -135,7 +135,7 @@ export const LOGS_FEATURE = { ui: ['show', 'configureSource', 'save'], }, read: { - app: ['infra', 'logs', 'kibana'], + app: ['infra', 'logs', 'kibana', 'observability-logs-explorer'], catalogue: ['infralogging', 'logs'], api: ['infra', 'rac'], alerting: { diff --git a/x-pack/plugins/observability_solution/infra/server/infra_server.ts b/x-pack/plugins/observability_solution/infra/server/infra_server.ts index 8206aa13750df..627c1346bee79 100644 --- a/x-pack/plugins/observability_solution/infra/server/infra_server.ts +++ b/x-pack/plugins/observability_solution/infra/server/infra_server.ts @@ -34,6 +34,7 @@ import { initProfilingRoutes } from './routes/profiling'; import { initServicesRoute } from './routes/services'; import { initCustomDashboardsRoutes } from './routes/custom_dashboards/custom_dashboards'; import { InfraBackendLibs } from './lib/infra_types'; +import { initEntitiesConfigurationRoutes } from './routes/entities'; export const registerRoutes = (libs: InfraBackendLibs) => { initIpToHostName(libs); @@ -63,4 +64,5 @@ export const registerRoutes = (libs: InfraBackendLibs) => { initProfilingRoutes(libs); initServicesRoute(libs); initCustomDashboardsRoutes(libs.framework); + initEntitiesConfigurationRoutes(libs); }; diff --git a/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/adapter_types.ts index 7b068424a7cc8..2cbf6b61623cf 100644 --- a/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/observability_solution/infra/server/lib/adapters/framework/adapter_types.ts @@ -39,6 +39,10 @@ import { ApmDataAccessPluginStart, } from '@kbn/apm-data-access-plugin/server'; import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/server'; +import type { + EntityManagerServerPluginStart, + EntityManagerServerPluginSetup, +} from '@kbn/entityManager-plugin/server'; export interface InfraServerPluginSetupDeps { alerting: AlertingPluginContract; @@ -56,6 +60,7 @@ export interface InfraServerPluginSetupDeps { metricsDataAccess: MetricsDataPluginSetup; profilingDataAccess?: ProfilingDataAccessPluginSetup; apmDataAccess: ApmDataAccessPluginSetup; + entityManager: EntityManagerServerPluginSetup; } export interface InfraServerPluginStartDeps { @@ -66,6 +71,7 @@ export interface InfraServerPluginStartDeps { ruleRegistry: RuleRegistryPluginStartContract; apmDataAccess: ApmDataAccessPluginStart; logsDataAccess: LogsDataAccessPluginStart; + entityManager: EntityManagerServerPluginStart; } export interface CallWithRequestParams extends estypes.RequestBase { diff --git a/x-pack/plugins/observability_solution/infra/server/plugin.ts b/x-pack/plugins/observability_solution/infra/server/plugin.ts index 6b1d1e4ea4b63..73d49ed938546 100644 --- a/x-pack/plugins/observability_solution/infra/server/plugin.ts +++ b/x-pack/plugins/observability_solution/infra/server/plugin.ts @@ -296,6 +296,7 @@ export class InfraServerPlugin const coreContext = await context.core; const savedObjectsClient = coreContext.savedObjects.client; const uiSettingsClient = coreContext.uiSettings.client; + const entityManager = await this.libs.plugins.entityManager.start(); const mlSystem = plugins.ml?.mlSystemProvider(request, savedObjectsClient); const mlAnomalyDetectors = plugins.ml?.anomalyDetectorsProvider( @@ -317,6 +318,7 @@ export class InfraServerPlugin savedObjectsClient, uiSettingsClient, getMetricsIndices, + entityManager, }; } ); diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts new file mode 100644 index 0000000000000..6422ab9502f55 --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/get_latest_entity.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { type ObservabilityElasticsearchClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { esqlResultToPlainObjects } from '@kbn/observability-utils/es/utils/esql_result_to_plain_objects'; +import { ENTITY_LATEST, EntityDefinition, entitiesAliasPattern } from '@kbn/entities-schema'; +import { type EntityDefinitionWithState } from '@kbn/entityManager-plugin/server/lib/entities/types'; +import { + ENTITY_TYPE, + SOURCE_DATA_STREAM_TYPE, +} from '@kbn/observability-shared-plugin/common/field_names/elasticsearch'; + +const ENTITIES_LATEST_ALIAS = entitiesAliasPattern({ + type: '*', + dataset: ENTITY_LATEST, +}); + +interface Entity { + [SOURCE_DATA_STREAM_TYPE]: string | string[]; +} + +export async function getLatestEntity({ + inventoryEsClient, + entityId, + entityType, + entityDefinitions, +}: { + inventoryEsClient: ObservabilityElasticsearchClient; + entityType: 'host' | 'container'; + entityId: string; + entityDefinitions: EntityDefinition[] | EntityDefinitionWithState[]; +}) { + const hostOrContainerIdentityField = entityDefinitions[0]?.identityFields?.[0]?.field; + if (hostOrContainerIdentityField === undefined) { + return; + } + const latestEntitiesEsqlResponse = await inventoryEsClient.esql('get_latest_entities', { + query: `FROM ${ENTITIES_LATEST_ALIAS} + | WHERE ${ENTITY_TYPE} == "${entityType}" + | WHERE ${hostOrContainerIdentityField} == "${entityId}" + | KEEP ${SOURCE_DATA_STREAM_TYPE} + `, + }); + + return esqlResultToPlainObjects(latestEntitiesEsqlResponse)[0]; +} diff --git a/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts new file mode 100644 index 0000000000000..2482f235faccc --- /dev/null +++ b/x-pack/plugins/observability_solution/infra/server/routes/entities/index.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; +import { METRICS_APP_ID } from '@kbn/deeplinks-observability/constants'; +import { SOURCE_DATA_STREAM_TYPE } from '@kbn/observability-shared-plugin/common/field_names/elasticsearch'; +import { createObservabilityEsClient } from '@kbn/observability-utils/es/client/create_observability_es_client'; +import { InfraBackendLibs } from '../../lib/infra_types'; +import { getLatestEntity } from './get_latest_entity'; + +export const initEntitiesConfigurationRoutes = (libs: InfraBackendLibs) => { + const { framework, logger } = libs; + + framework.registerRoute( + { + method: 'get', + path: '/api/infra/entities/{entityType}/{entityId}/summary', + validate: { + params: schema.object({ + entityType: schema.oneOf([schema.literal('host'), schema.literal('container')]), + entityId: schema.string(), + }), + }, + options: { + access: 'internal', + }, + }, + async (requestContext, request, response) => { + const { entityId, entityType } = request.params; + const coreContext = await requestContext.core; + const infraContext = await requestContext.infra; + const entityManager = await infraContext.entityManager.getScopedClient({ request }); + + const client = createObservabilityEsClient({ + client: coreContext.elasticsearch.client.asCurrentUser, + logger, + plugin: `@kbn/${METRICS_APP_ID}-plugin`, + }); + + try { + // Only fetch built in definitions + const { definitions } = await entityManager.getEntityDefinitions({ + builtIn: true, + type: entityType, + }); + if (definitions.length === 0) { + return response.ok({ + body: { sourceDataStreams: [], entityId, entityType }, + }); + } + + const entity = await getLatestEntity({ + inventoryEsClient: client, + entityId, + entityType, + entityDefinitions: definitions, + }); + + return response.ok({ + body: { + sourceDataStreams: [entity?.[SOURCE_DATA_STREAM_TYPE] || []].flat() as string[], + entityId, + entityType, + }, + }); + } catch (error) { + return response.customError({ + statusCode: error.statusCode ?? 500, + body: { + message: error.message ?? 'An unexpected error occurred', + }, + }); + } + } + ); +}; diff --git a/x-pack/plugins/observability_solution/infra/server/types.ts b/x-pack/plugins/observability_solution/infra/server/types.ts index ed48814a0cb30..3f5f14c807d40 100644 --- a/x-pack/plugins/observability_solution/infra/server/types.ts +++ b/x-pack/plugins/observability_solution/infra/server/types.ts @@ -13,6 +13,7 @@ import type { } from '@kbn/core/server'; import type { SearchRequestHandlerContext } from '@kbn/data-plugin/server'; import type { MlPluginSetup } from '@kbn/ml-plugin/server'; +import type { EntityManagerServerPluginStart } from '@kbn/entityManager-plugin/server'; import { InfraServerPluginStartDeps } from './lib/adapters/framework'; import { InventoryViewsServiceSetup, InventoryViewsServiceStart } from './services/inventory_views'; import { @@ -45,6 +46,7 @@ export interface InfraRequestHandlerContext { savedObjectsClient: SavedObjectsClientContract; uiSettingsClient: IUiSettingsClient; getMetricsIndices: () => Promise; + entityManager: EntityManagerServerPluginStart; } /** diff --git a/x-pack/plugins/observability_solution/infra/tsconfig.json b/x-pack/plugins/observability_solution/infra/tsconfig.json index 45a03b23b4b9b..e7aade296fa8a 100644 --- a/x-pack/plugins/observability_solution/infra/tsconfig.json +++ b/x-pack/plugins/observability_solution/infra/tsconfig.json @@ -109,7 +109,10 @@ "@kbn/core-analytics-browser", "@kbn/observability-alerting-rule-utils", "@kbn/core-application-browser", - "@kbn/shared-ux-page-no-data-types" + "@kbn/shared-ux-page-no-data-types", + "@kbn/entityManager-plugin", + "@kbn/observability-utils", + "@kbn/entities-schema" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/inventory/common/entities.ts b/x-pack/plugins/observability_solution/inventory/common/entities.ts index 9b1f0670eda5e..01f17807c3486 100644 --- a/x-pack/plugins/observability_solution/inventory/common/entities.ts +++ b/x-pack/plugins/observability_solution/inventory/common/entities.ts @@ -4,24 +4,22 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as t from 'io-ts'; import { ENTITY_LATEST, entitiesAliasPattern } from '@kbn/entities-schema'; -import { isRight } from 'fp-ts/lib/Either'; import { - SERVICE_ENVIRONMENT, - SERVICE_NAME, CONTAINER_ID, HOST_NAME, AGENT_NAME, CLOUD_PROVIDER, -} from '@kbn/observability-shared-plugin/common'; -import { ENTITY_DEFINITION_ID, ENTITY_DISPLAY_NAME, ENTITY_ID, ENTITY_LAST_SEEN, ENTITY_TYPE, -} from './es_fields/entities'; + SERVICE_ENVIRONMENT, + SERVICE_NAME, +} from '@kbn/observability-shared-plugin/common'; +import { isRight } from 'fp-ts/lib/Either'; +import * as t from 'io-ts'; export const entityTypeRt = t.union([ t.literal('service'), diff --git a/x-pack/plugins/observability_solution/inventory/common/es_fields/entities.ts b/x-pack/plugins/observability_solution/inventory/common/es_fields/entities.ts deleted file mode 100644 index 9b619dddbb2df..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/common/es_fields/entities.ts +++ /dev/null @@ -1,12 +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. - */ - -export const ENTITY_LAST_SEEN = 'entity.lastSeenTimestamp'; -export const ENTITY_ID = 'entity.id'; -export const ENTITY_TYPE = 'entity.type'; -export const ENTITY_DISPLAY_NAME = 'entity.displayName'; -export const ENTITY_DEFINITION_ID = 'entity.definitionId'; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/badge_filter_with_popover/badge_filter_with_popover.test.tsx b/x-pack/plugins/observability_solution/inventory/public/components/badge_filter_with_popover/badge_filter_with_popover.test.tsx index cda2f0bcb42d3..6018b66d37991 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/badge_filter_with_popover/badge_filter_with_popover.test.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/badge_filter_with_popover/badge_filter_with_popover.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { render, fireEvent, screen } from '@testing-library/react'; import { BadgeFilterWithPopover } from '.'; import { EuiThemeProvider, copyToClipboard } from '@elastic/eui'; -import { ENTITY_TYPE } from '../../../common/es_fields/entities'; +import { ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; jest.mock('@elastic/eui', () => ({ ...jest.requireActual('@elastic/eui'), diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx index f05bf920a1845..1f4d3f1f34b40 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entities_grid.stories.tsx @@ -9,9 +9,9 @@ import { EuiDataGridSorting, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic import { Meta, Story } from '@storybook/react'; import { orderBy } from 'lodash'; import React, { useMemo, useState } from 'react'; +import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '@kbn/observability-shared-plugin/common'; import { EntitiesGrid } from '.'; import { EntityType } from '../../../common/entities'; -import { ENTITY_LAST_SEEN, ENTITY_TYPE } from '../../../common/es_fields/entities'; import { entitiesMock } from './mock/entities_mock'; const stories: Meta<{}> = { diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx index 9d39c86e2d8ee..28fe38511fa9f 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/entity_name/index.tsx @@ -11,10 +11,11 @@ import { AssetDetailsLocatorParams, ASSET_DETAILS_LOCATOR_ID, ServiceOverviewParams, + ENTITY_TYPE, + ENTITY_DISPLAY_NAME, } from '@kbn/observability-shared-plugin/common'; import { useKibana } from '../../../hooks/use_kibana'; import { EntityIcon } from '../../entity_icon'; -import { ENTITY_DISPLAY_NAME, ENTITY_TYPE } from '../../../../common/es_fields/entities'; import { Entity } from '../../../../common/entities'; import { parseServiceParams } from '../../../utils/parse_service_params'; diff --git a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx index 05c35f65a9f99..8bdfa0d46627c 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/entities_grid/index.tsx @@ -18,16 +18,16 @@ import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react'; import { last } from 'lodash'; import React, { useCallback, useState } from 'react'; -import type { EntityType } from '../../../common/entities'; import { ENTITY_DISPLAY_NAME, ENTITY_LAST_SEEN, ENTITY_TYPE, -} from '../../../common/es_fields/entities'; -import type { APIReturnType } from '../../api'; -import { getEntityTypeLabel } from '../../utils/get_entity_type_label'; +} from '@kbn/observability-shared-plugin/common'; +import { APIReturnType } from '../../api'; import { BadgeFilterWithPopover } from '../badge_filter_with_popover'; import { EntityName } from './entity_name'; +import { EntityType } from '../../../common/entities'; +import { getEntityTypeLabel } from '../../utils/get_entity_type_label'; type InventoryEntitiesAPIReturnType = APIReturnType<'GET /internal/inventory/entities'>; type LatestEntities = InventoryEntitiesAPIReturnType['entities']; @@ -147,7 +147,7 @@ export function EntitiesGrid({ const columnEntityTableId = columnId as EntityColumnIds; switch (columnEntityTableId) { case ENTITY_TYPE: - const entityType = entity[columnEntityTableId] as EntityType; + const entityType = entity[columnEntityTableId]; return ( `WHERE ${ENTITY_TYPE} IN (${entityTypes.map((entityType) => `"${entityType}"`).join()})`; diff --git a/x-pack/plugins/observability_solution/observability/public/components/alert_sources/get_sources.ts b/x-pack/plugins/observability_solution/observability/public/components/alert_sources/get_sources.ts index 3832e744a31e6..836967ea88aa4 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alert_sources/get_sources.ts +++ b/x-pack/plugins/observability_solution/observability/public/components/alert_sources/get_sources.ts @@ -8,27 +8,33 @@ import { ALERT_GROUP_FIELD, ALERT_GROUP_VALUE, ALERT_GROUP } from '@kbn/rule-data-utils'; import { TopAlert } from '../../typings/alerts'; import { apmSources, infraSources } from './get_alert_source_links'; +import { Group } from '../../../common/typings'; interface AlertFields { [key: string]: any; } -export const getSources = (alert: TopAlert) => { +const isGroup = (item: Group | undefined): item is Group => { + return !!item; +}; + +export const getSources = (alert: TopAlert): Group[] => { // when `kibana.alert.group` is not flattened (for alert detail pages) - if (alert.fields[ALERT_GROUP]) return alert.fields[ALERT_GROUP]; + if (alert.fields[ALERT_GROUP]) return alert.fields[ALERT_GROUP] as Group[]; // when `kibana.alert.group` is flattened (for alert flyout) const groupsFromGroupFields = alert.fields[ALERT_GROUP_FIELD]?.map((field, index) => { const values = alert.fields[ALERT_GROUP_VALUE]; if (values?.length && values[index]) { - return { field, value: values[index] }; + const group: Group = { field, value: values[index] }; + return group; } - }); + }).filter(isGroup); if (groupsFromGroupFields?.length) return groupsFromGroupFields; // Not all rules has group.fields, in that case we search in the alert fields. - const matchedSources: Array<{ field: string; value: any }> = []; + const matchedSources: Group[] = []; const ALL_SOURCES = [...infraSources, ...apmSources]; const alertFields = alert.fields as AlertFields; ALL_SOURCES.forEach((source: string) => { diff --git a/x-pack/plugins/observability_solution/observability/public/components/alert_sources/groups.tsx b/x-pack/plugins/observability_solution/observability/public/components/alert_sources/groups.tsx index 5a14f39a97811..17c596b146d1a 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/alert_sources/groups.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/alert_sources/groups.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiLink } from '@elastic/eui'; +import { EuiLink, EuiText } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; import { SERVICE_NAME } from '@kbn/observability-shared-plugin/common'; import { useKibana } from '../../utils/kibana_react'; @@ -54,7 +54,7 @@ export function Groups({ groups, timeRange }: { groups: Group[]; timeRange: Time {groups && groups.map((group) => { return ( - + {group.field}:{' '} {sourceLinks[group.field] ? ( @@ -63,8 +63,7 @@ export function Groups({ groups, timeRange }: { groups: Group[]; timeRange: Time ) : ( {group.value} )} -
-
+ ); })} diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx index d16df6b6120df..de74fe2ec14b9 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.test.tsx @@ -103,7 +103,7 @@ describe('AlertDetailsAppSection', () => { const result = renderComponent(); expect((await result.findByTestId('thresholdAlertOverviewSection')).children.length).toBe(6); - expect(result.getByTestId('thresholdRule-2000-2500')).toBeTruthy(); + expect(result.getByTestId('threshold-2000-2500')).toBeTruthy(); }); it('should render annotations', async () => { diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx index a7ec41f4ab660..7885301650ecf 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/alert_details_app_section/alert_details_app_section.tsx @@ -38,7 +38,7 @@ import { useLicense } from '../../../../hooks/use_license'; import { useKibana } from '../../../../utils/kibana_react'; import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter'; import { AlertParams } from '../../types'; -import { Threshold } from '../custom_threshold'; +import { Threshold } from '../threshold'; import { CustomThresholdRule, CustomThresholdAlert } from '../types'; import { LogRateAnalysis } from './log_rate_analysis'; import { RuleConditionChart } from '../../../rule_condition_chart/rule_condition_chart'; diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.stories.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.stories.tsx index f471333f53661..8eeca2e107b5f 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.stories.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { ComponentMeta } from '@storybook/react'; import { LIGHT_THEME } from '@elastic/charts'; import { COMPARATORS } from '@kbn/alerting-comparators'; -import { Props, Threshold as Component } from './custom_threshold'; +import { Props, Threshold as Component } from './threshold'; export default { component: Component, @@ -35,7 +35,7 @@ const defaultProps: Props = { threshold: [90], title: 'Threshold breached', value: 93, - valueFormatter: (d) => `${d}%`, + valueFormatter: (d: number) => `${d}%`, }; export const Default = { diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.test.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.test.tsx similarity index 85% rename from x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.test.tsx rename to x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.test.tsx index e0c83d31ddb4c..9e592f2336dc1 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.test.tsx @@ -7,10 +7,10 @@ import { LIGHT_THEME } from '@elastic/charts'; -import { render } from '@testing-library/react'; -import { Props, Threshold } from './custom_threshold'; import React from 'react'; +import { render } from '@testing-library/react'; import { COMPARATORS } from '@kbn/alerting-comparators'; +import { Props, Threshold } from './threshold'; describe('Threshold', () => { const renderComponent = (props: Partial = {}) => { @@ -38,7 +38,7 @@ describe('Threshold', () => { it('shows component', () => { const component = renderComponent(); - expect(component.queryByTestId('thresholdRule-90-93')).toBeTruthy(); + expect(component.queryByTestId('threshold-90-93')).toBeTruthy(); }); it('shows component for between', () => { @@ -46,6 +46,6 @@ describe('Threshold', () => { comparator: COMPARATORS.BETWEEN, threshold: [90, 95], }); - expect(component.queryByTestId('thresholdRule-90-95-93')).toBeTruthy(); + expect(component.queryByTestId('threshold-90-95-93')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.tsx b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.tsx similarity index 88% rename from x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.tsx rename to x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.tsx index a82c369c57737..347730fe4dea6 100644 --- a/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/custom_threshold.tsx +++ b/x-pack/plugins/observability_solution/observability/public/components/custom_threshold/components/threshold.tsx @@ -6,14 +6,14 @@ */ import React from 'react'; -import { Chart, Metric, Settings } from '@elastic/charts'; +import { Chart, Metric, Settings, ValueFormatter } from '@elastic/charts'; import { EuiIcon, EuiPanel, useEuiBackgroundColor } from '@elastic/eui'; import type { PartialTheme, Theme } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { COMPARATORS } from '@kbn/alerting-comparators'; export interface ChartProps { - theme?: PartialTheme; + theme?: PartialTheme[]; baseTheme: Theme; } @@ -24,7 +24,7 @@ export interface Props { threshold: number[]; title: string; value: number; - valueFormatter: (d: number) => string; + valueFormatter?: ValueFormatter; } export function Threshold({ @@ -34,7 +34,7 @@ export function Threshold({ threshold, title, value, - valueFormatter, + valueFormatter = (d) => String(d), }: Props) { const color = useEuiBackgroundColor('danger'); @@ -42,13 +42,14 @@ export function Threshold({ diff --git a/x-pack/plugins/observability_solution/observability/public/index.ts b/x-pack/plugins/observability_solution/observability/public/index.ts index 512e8e7e75bf9..58c3aa4cadd66 100644 --- a/x-pack/plugins/observability_solution/observability/public/index.ts +++ b/x-pack/plugins/observability_solution/observability/public/index.ts @@ -63,9 +63,9 @@ export const LazyAlertsFlyout = lazy(() => import('./components/alerts_flyout/al export * from './typings'; import { TopAlert } from './typings/alerts'; -import { AlertSummary } from './pages/alert_details/components'; -import type { AlertSummaryField } from './pages/alert_details/components/alert_summary'; -export type { TopAlert, AlertSummary, AlertSummaryField }; +export type { TopAlert }; +import type { AlertDetailsAppSectionProps } from './pages/alert_details/types'; +export type { AlertDetailsAppSectionProps }; export { observabilityFeatureId, observabilityAppId } from '../common'; @@ -102,3 +102,4 @@ export { useAnnotations } from './components/annotations/use_annotations'; export { RuleConditionChart } from './components/rule_condition_chart'; export { getGroupFilters } from '../common/custom_threshold_rule/helpers/get_group'; export type { GenericAggType } from './components/rule_condition_chart/rule_condition_chart'; +export { Threshold } from './components/custom_threshold/components/threshold'; diff --git a/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts b/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts index 353f1258fc7b8..c661976fd0765 100644 --- a/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts +++ b/x-pack/plugins/observability_solution/observability/public/navigation_tree.ts @@ -68,66 +68,10 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { link: 'slo', }, { - id: 'aiMl', - title: i18n.translate('xpack.observability.obltNav.ml.aiAndMlGroupTitle', { - defaultMessage: 'AI & ML', + link: 'observabilityAIAssistant', + title: i18n.translate('xpack.observability.obltNav.aiMl.aiAssistant', { + defaultMessage: 'AI Assistant', }), - renderAs: 'accordion', - children: [ - { - link: 'observabilityAIAssistant', - title: i18n.translate('xpack.observability.obltNav.aiMl.aiAssistant', { - defaultMessage: 'AI Assistant', - }), - }, - { - link: 'ml:anomalyDetection', - renderAs: 'item', - children: [ - { - link: 'ml:singleMetricViewer', - }, - { - link: 'ml:anomalyExplorer', - }, - { - link: 'ml:settings', - }, - ], - }, - { - title: i18n.translate('xpack.observability.obltNav.ml.logRateAnalysis', { - defaultMessage: 'Log rate analysis', - }), - link: 'ml:logRateAnalysis', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.includes(prepend('/app/ml/aiops/log_rate_analysis')); - }, - }, - { - link: 'logs:anomalies', - }, - { - link: 'logs:log-categories', - }, - { - title: i18n.translate('xpack.observability.obltNav.ml.changePointDetection', { - defaultMessage: 'Change point detection', - }), - link: 'ml:changePointDetections', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.includes( - prepend('/app/ml/aiops/change_point_detection') - ); - }, - }, - { - title: i18n.translate('xpack.observability.obltNav.ml.job.notifications', { - defaultMessage: 'Job notifications', - }), - link: 'ml:notifications', - }, - ], }, { link: 'inventory', @@ -138,33 +82,26 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { title: i18n.translate('xpack.observability.obltNav.applications', { defaultMessage: 'Applications', }), - renderAs: 'accordion', + renderAs: 'panelOpener', children: [ { - link: 'apm:services', - getIsActive: ({ pathNameSerialized }) => { - const regex = /app\/apm\/.*service.*/; - return regex.test(pathNameSerialized); - }, - }, - { - link: 'apm:traces', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/apm/traces')); - }, - }, - { - link: 'apm:dependencies', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/apm/dependencies')); - }, + children: [ + { link: 'apm:services' }, + { link: 'apm:traces' }, + { link: 'apm:dependencies' }, + { + link: 'ux', + title: i18n.translate('xpack.observability.obltNav.apm.ux', { + defaultMessage: 'User experience', + }), + }, + ], }, { id: 'synthetics', title: i18n.translate('xpack.observability.obltNav.apm.syntheticsGroupTitle', { defaultMessage: 'Synthetics', }), - renderAs: 'accordion', children: [ { link: 'synthetics', @@ -172,10 +109,17 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { defaultMessage: 'Monitors', }), }, - { link: 'synthetics:certificates' }, + { + link: 'synthetics:certificates', + title: i18n.translate( + 'xpack.observability.obltNav.apm.synthetics.tlsCertificates', + { + defaultMessage: 'TLS certificates', + } + ), + }, ], }, - { link: 'ux' }, ], }, { @@ -183,32 +127,36 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { title: i18n.translate('xpack.observability.obltNav.infrastructure', { defaultMessage: 'Infrastructure', }), - renderAs: 'accordion', + renderAs: 'panelOpener', children: [ { - link: 'metrics:inventory', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/metrics/inventory')); - }, - }, - { - link: 'metrics:hosts', - getIsActive: ({ pathNameSerialized, prepend }) => { - return pathNameSerialized.startsWith(prepend('/app/metrics/hosts')); - }, - }, - { - link: 'metrics:metrics-explorer', + children: [ + { + link: 'metrics:inventory', + title: i18n.translate('xpack.observability.infrastructure.inventory', { + defaultMessage: 'Infrastructure inventory', + }), + }, + { link: 'metrics:hosts' }, + { + link: 'metrics:metrics-explorer', + title: i18n.translate( + 'xpack.observability.obltNav.infrastructure.metricsExplorer', + { + defaultMessage: 'Metrics explorer', + } + ), + }, + ], }, { id: 'profiling', title: i18n.translate( 'xpack.observability.obltNav.infrastructure.universalProfiling', { - defaultMessage: 'Universal Profiling', + defaultMessage: 'Universal profiling', } ), - renderAs: 'accordion', children: [ { link: 'profiling:stacktraces', @@ -223,138 +171,12 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { }, ], }, - { - id: 'otherTools', - title: i18n.translate('xpack.observability.obltNav.otherTools', { - defaultMessage: 'Other tools', - }), - renderAs: 'accordion', - children: [ - { - link: 'logs:stream', - title: i18n.translate('xpack.observability.obltNav.otherTools.logsStream', { - defaultMessage: 'Logs stream', - }), - }, - { link: 'maps' }, - { link: 'canvas' }, - { link: 'graph' }, - ], - }, - ], - }, - ], - footer: [ - { type: 'recentlyAccessed' }, - { - type: 'navItem', - title: i18n.translate('xpack.observability.obltNav.getStarted', { - defaultMessage: 'Get started', - }), - link: 'observabilityOnboarding', - icon: 'launch', - }, - { - type: 'navItem', - id: 'devTools', - title: i18n.translate('xpack.observability.obltNav.devTools', { - defaultMessage: 'Developer tools', - }), - link: 'dev_tools', - icon: 'editorCodeBlock', - }, - { - type: 'navGroup', - id: 'project_settings_project_nav', - title: i18n.translate('xpack.observability.obltNav.management', { - defaultMessage: 'Management', - }), - icon: 'gear', - breadcrumbStatus: 'hidden', - children: [ - { - id: 'stack_management', // This id can't be changed as we use it to open the panel programmatically - link: 'management', - title: i18n.translate('xpack.observability.obltNav.stackManagement', { - defaultMessage: 'Stack Management', - }), - renderAs: 'panelOpener', - spaceBefore: null, - children: [ - { - title: 'Ingest', - children: [ - { link: 'management:ingest_pipelines' }, - { link: 'management:pipelines' }, - ], - }, - { - title: 'Data', - children: [ - { link: 'management:index_management' }, - { link: 'management:index_lifecycle_management' }, - { link: 'management:snapshot_restore' }, - { link: 'management:rollup_jobs' }, - { link: 'management:transform' }, - { link: 'management:cross_cluster_replication' }, - { link: 'management:remote_clusters' }, - { link: 'management:migrate_data' }, - ], - }, - { - title: 'Alerts and Insights', - children: [ - { link: 'management:triggersActions' }, - { link: 'management:cases' }, - { link: 'management:triggersActionsConnectors' }, - { link: 'management:reporting' }, - { link: 'management:jobsListLink' }, - { link: 'management:watcher' }, - { link: 'management:maintenanceWindows' }, - ], - }, - { - title: 'Security', - children: [ - { link: 'management:users' }, - { link: 'management:roles' }, - { link: 'management:api_keys' }, - { link: 'management:role_mappings' }, - ], - }, - { - title: 'Kibana', - children: [ - { link: 'management:dataViews' }, - { link: 'management:filesManagement' }, - { link: 'management:objects' }, - { link: 'management:tags' }, - { link: 'management:search_sessions' }, - { link: 'management:aiAssistantManagementSelection' }, - { link: 'management:spaces' }, - { link: 'management:settings' }, - ], - }, - { - title: 'Stack', - children: [ - { link: 'management:license_management' }, - { link: 'management:upgrade_assistant' }, - ], - }, - ], - }, - { - link: 'integrations', - }, - { - link: 'fleet', - }, { id: 'machine_learning-landing', - link: 'securitySolutionUI:machine_learning-landing', renderAs: 'panelOpener', - spaceBefore: null, + title: i18n.translate('xpack.observability.obltNav.machineLearning', { + defaultMessage: 'Machine learning', + }), children: [ { children: [ @@ -366,6 +188,12 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { }, { link: 'ml:memoryUsage', + title: i18n.translate( + 'xpack.observability.obltNav.machineLearning.memoryUsage', + { + defaultMessage: 'Memory usage', + } + ), }, ], }, @@ -391,6 +219,9 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { { link: 'ml:settings', }, + { + link: 'ml:suppliedConfigurations', + }, ], }, { @@ -426,6 +257,12 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { children: [ { link: 'ml:nodesOverview', + title: i18n.translate( + 'xpack.observability.obltNav.ml.model_management.trainedModels', + { + defaultMessage: 'Trained models', + } + ), }, ], }, @@ -448,7 +285,7 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { { link: 'ml:indexDataVisualizer', title: i18n.translate( - 'xpack.observability.obltNav.ml.data_visualizer.file_data_visualizer', + 'xpack.observability.obltNav.ml.data_visualizer.data_view_data_visualizer', { defaultMessage: 'Data view data visualizer', } @@ -456,6 +293,12 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { }, { link: 'ml:dataDrift', + title: i18n.translate( + 'xpack.observability.obltNav.ml.data_visualizer.data_drift', + { + defaultMessage: 'Data drift', + } + ), }, ], }, @@ -468,17 +311,182 @@ export function createNavTree(pluginsStart: ObservabilityPublicPluginsStart) { children: [ { link: 'ml:logRateAnalysis', + title: i18n.translate( + 'xpack.observability.obltNav.ml.aiops_labs.log_rate_analysis', + { + defaultMessage: 'Log rate analysis', + } + ), }, { link: 'ml:logPatternAnalysis', + title: i18n.translate( + 'xpack.observability.obltNav.ml.aiops_labs.log_pattern_analysis', + { + defaultMessage: 'Log pattern analysis', + } + ), }, { link: 'ml:changePointDetections', + title: i18n.translate( + 'xpack.observability.obltNav.ml.aiops_labs.change_point_detection', + { + defaultMessage: 'Change point detection', + } + ), }, ], }, ], }, + { + id: 'otherTools', + title: i18n.translate('xpack.observability.obltNav.otherTools', { + defaultMessage: 'Other tools', + }), + renderAs: 'panelOpener', + icon: 'editorCodeBlock', + children: [ + { + link: 'logs:stream', + title: i18n.translate('xpack.observability.obltNav.otherTools.logsStream', { + defaultMessage: 'Logs stream', + }), + }, + { + link: 'logs:anomalies', + title: i18n.translate('xpack.observability.obltNav.otherTools.logsAnomalies', { + defaultMessage: 'Logs anomalies', + }), + }, + { + link: 'logs:log-categories', + title: i18n.translate('xpack.observability.obltNav.otherTools.logsCategories', { + defaultMessage: 'Logs categories', + }), + }, + { link: 'maps' }, + { link: 'canvas' }, + { link: 'graph' }, + { + link: 'visualize', + title: i18n.translate('xpack.observability.obltNav.otherTools.logsCategories', { + defaultMessage: 'Visualize library', + }), + }, + ], + }, + ], + }, + ], + footer: [ + { type: 'recentlyAccessed' }, + { + type: 'navItem', + title: i18n.translate('xpack.observability.obltNav.addData', { + defaultMessage: 'Add data', + }), + link: 'observabilityOnboarding', + icon: 'launch', + }, + { + type: 'navItem', + id: 'devTools', + title: i18n.translate('xpack.observability.obltNav.devTools', { + defaultMessage: 'Developer tools', + }), + link: 'dev_tools', + icon: 'editorCodeBlock', + }, + { + type: 'navGroup', + id: 'project_settings_project_nav', + title: i18n.translate('xpack.observability.obltNav.management', { + defaultMessage: 'Management', + }), + icon: 'gear', + breadcrumbStatus: 'hidden', + children: [ + { + id: 'stack_management', // This id can't be changed as we use it to open the panel programmatically + link: 'management', + title: i18n.translate('xpack.observability.obltNav.stackManagement', { + defaultMessage: 'Stack Management', + }), + renderAs: 'panelOpener', + spaceBefore: null, + children: [ + { + title: 'Ingest', + children: [ + { link: 'management:ingest_pipelines' }, + { link: 'management:pipelines' }, + ], + }, + { + title: 'Data', + children: [ + { link: 'management:index_management' }, + { link: 'management:data_quality' }, + { link: 'management:index_lifecycle_management' }, + { link: 'management:snapshot_restore' }, + { link: 'management:rollup_jobs' }, + { link: 'management:transform' }, + { link: 'management:cross_cluster_replication' }, + { link: 'management:remote_clusters' }, + { link: 'management:migrate_data' }, + ], + }, + { + title: 'Alerts and Insights', + children: [ + { link: 'management:triggersActions' }, + { link: 'management:cases' }, + { link: 'management:triggersActionsConnectors' }, + { link: 'management:reporting' }, + { link: 'management:jobsListLink' }, + { link: 'management:watcher' }, + { link: 'management:maintenanceWindows' }, + ], + }, + { + title: 'Security', + children: [ + { link: 'management:users' }, + { link: 'management:roles' }, + { link: 'management:api_keys' }, + { link: 'management:role_mappings' }, + ], + }, + { + title: 'Kibana', + children: [ + { link: 'management:dataViews' }, + { link: 'management:filesManagement' }, + { link: 'management:objects' }, + { link: 'management:tags' }, + { link: 'management:search_sessions' }, + { link: 'management:aiAssistantManagementSelection' }, + { link: 'management:spaces' }, + { link: 'management:settings' }, + ], + }, + { + title: 'Stack', + children: [ + { link: 'management:license_management' }, + { link: 'management:upgrade_assistant' }, + ], + }, + ], + }, + { + link: 'integrations', + }, + { + link: 'fleet', + }, { id: 'cloudLinkUserAndRoles', cloudLink: 'userAndRoles', diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx index 877b0b965d1ce..be62ae6377bc6 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.test.tsx @@ -9,7 +9,7 @@ import { casesPluginMock } from '@kbn/cases-plugin/public/mocks'; import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting'; import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assistant-plugin/public/mock'; -import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; +import { useBreadcrumbs, TagsList } from '@kbn/observability-shared-plugin/public'; import { RuleTypeModel, ValidationResult } from '@kbn/triggers-actions-ui-plugin/public'; import { ruleTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/rule_type_registry.mock'; import { waitFor } from '@testing-library/react'; @@ -88,6 +88,7 @@ const useParamsMock = useParams as jest.Mock; const useLocationMock = useLocation as jest.Mock; const useHistoryMock = useHistory as jest.Mock; const useBreadcrumbsMock = useBreadcrumbs as jest.Mock; +const TagsListMock = TagsList as jest.Mock; const chance = new Chance(); @@ -114,6 +115,7 @@ describe('Alert details', () => { useLocationMock.mockReturnValue({ pathname: '/alerts/uuid', search: '', state: '', hash: '' }); useHistoryMock.mockReturnValue({ replace: jest.fn() }); useBreadcrumbsMock.mockReturnValue([]); + TagsListMock.mockReturnValue(
); ruleTypeRegistry.list.mockReturnValue([ruleType]); ruleTypeRegistry.get.mockReturnValue(ruleType); ruleTypeRegistry.has.mockReturnValue(true); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx index e7533c226df58..fbe1858858d05 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/alert_details.tsx @@ -16,6 +16,7 @@ import { EuiLoadingSpinner, EuiTabbedContentTab, useEuiTheme, + EuiFlexGroup, } from '@elastic/eui'; import { AlertStatus, @@ -31,15 +32,16 @@ import dedent from 'dedent'; import { AlertFieldsTable } from '@kbn/alerts-ui-shared'; import { css } from '@emotion/react'; import { omit } from 'lodash'; +import { AlertDetailsSource } from './types'; +import { SourceBar } from './components'; +import { StatusBar } from './components/status_bar'; import { observabilityFeatureId } from '../../../common'; import { RelatedAlerts } from './components/related_alerts'; import { useKibana } from '../../utils/kibana_react'; import { useFetchRule } from '../../hooks/use_fetch_rule'; import { usePluginContext } from '../../hooks/use_plugin_context'; import { AlertData, useFetchAlertDetail } from '../../hooks/use_fetch_alert_detail'; -import { PageTitleContent } from './components/page_title_content'; import { HeaderActions } from './components/header_actions'; -import { AlertSummary, AlertSummaryField } from './components/alert_summary'; import { CenterJustifiedSpinner } from '../../components/center_justified_spinner'; import { getTimeZone } from '../../utils/get_time_zone'; import { isAlertDetailsEnabledPerApp } from '../../utils/is_alert_details_enabled'; @@ -103,10 +105,10 @@ export function AlertDetails() { const { rule } = useFetchRule({ ruleId, }); - const [summaryFields, setSummaryFields] = useState(); const [alertStatus, setAlertStatus] = useState(); const { euiTheme } = useEuiTheme(); + const [sources, setSources] = useState(); const [relatedAlertsKuery, setRelatedAlertsKuery] = useState(); const [activeTabId, setActiveTabId] = useState(() => { const searchParams = new URLSearchParams(search); @@ -212,17 +214,19 @@ export function AlertDetails() { */ isAlertDetailsEnabledPerApp(alertDetail.formatted, config) ? ( <> - - - - + + + + + + {rule && alertDetail.formatted && ( <> @@ -290,13 +294,6 @@ export function AlertDetails() { ) : ( ), - children: ( - - ), rightSideItems: [ , ], bottomBorder: false, + 'data-test-subj': rule?.ruleTypeId || 'alertDetailsPageTitle', }} pageSectionProps={{ paddingSize: 'none', @@ -321,6 +319,8 @@ export function AlertDetails() { }} data-test-subj="alertDetails" > + + ({ - ...jest.requireActual('react-router-dom'), - useParams: jest.fn(), -})); - -jest.mock('../../../utils/kibana_react'); - -const useKibanaMock = useKibana as jest.Mock; - -const mockKibana = () => { - useKibanaMock.mockReturnValue({ - services: { - ...kibanaStartMock.startContract().services, - http: { - basePath: { - prepend: jest.fn(), - }, - }, - }, - }); -}; - -describe('Alert summary', () => { - jest - .spyOn(useUiSettingHook, 'useUiSetting') - .mockImplementation(() => 'MMM D, YYYY @ HH:mm:ss.SSS'); - - beforeEach(() => { - jest.clearAllMocks(); - mockKibana(); - }); - - it('should show alert data', async () => { - const alertSummary = render( - - ); - - const groups = alertWithGroupsAndTags.fields[ALERT_GROUP] as Group[]; - - expect(alertSummary.queryByText('Source')).toBeInTheDocument(); - expect(alertSummary.queryByText(groups[0].field, { exact: false })).toBeInTheDocument(); - expect(alertSummary.queryByText(groups[0].value)).toBeInTheDocument(); - expect(alertSummary.queryByText(groups[1].field, { exact: false })).toBeInTheDocument(); - expect(alertSummary.queryByText(groups[1].value)).toBeInTheDocument(); - expect(alertSummary.queryByText('Tags')).toBeInTheDocument(); - expect(alertSummary.queryByText(alertWithGroupsAndTags.fields[TAGS]![0])).toBeInTheDocument(); - expect(alertSummary.queryByText('Rule')).toBeInTheDocument(); - expect( - alertSummary.queryByText(alertWithGroupsAndTags.fields[ALERT_RULE_NAME]) - ).toBeInTheDocument(); - expect(alertSummary.queryByText('Actual value')).toBeInTheDocument(); - expect(alertSummary.queryByText(alertWithGroupsAndTags.fields[ALERT_EVALUATION_VALUE]!)); - expect(alertSummary.queryByText('Expected value')).toBeInTheDocument(); - expect(alertSummary.queryByText(alertWithGroupsAndTags.fields[ALERT_EVALUATION_THRESHOLD]!)); - }); -}); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/alert_summary.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/alert_summary.tsx deleted file mode 100644 index 7738b27089a57..0000000000000 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/alert_summary.tsx +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect, useState, ReactNode } from 'react'; -import { EuiFlexItem, EuiFlexGroup, EuiText, EuiSpacer, EuiLink } from '@elastic/eui'; -import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; -import { - TAGS, - ALERT_START, - ALERT_END, - ALERT_RULE_NAME, - ALERT_RULE_UUID, -} from '@kbn/rule-data-utils'; -import { i18n } from '@kbn/i18n'; -import { TimeRange } from '@kbn/es-query'; -import { TopAlert } from '../../..'; -import { Groups } from '../../../components/alert_sources/groups'; -import { Tags } from '../../../components/tags'; -import { getSources } from '../../../components/alert_sources/get_sources'; -import { useKibana } from '../../../utils/kibana_react'; -import { paths } from '../../../../common/locators/paths'; - -export interface AlertSummaryField { - label: ReactNode | string; - value: ReactNode | string | number; -} -export interface AlertSummaryProps { - alert: TopAlert; - alertSummaryFields?: AlertSummaryField[]; -} - -export function AlertSummary({ alert, alertSummaryFields }: AlertSummaryProps) { - const { http } = useKibana().services; - - const [timeRange, setTimeRange] = useState({ from: 'now-15m', to: 'now' }); - - const alertStart = alert.fields[ALERT_START]; - const alertEnd = alert.fields[ALERT_END]; - const ruleName = alert.fields[ALERT_RULE_NAME]; - const ruleId = alert.fields[ALERT_RULE_UUID]; - const tags = alert.fields[TAGS]; - - const ruleLink = http.basePath.prepend(paths.observability.ruleDetails(ruleId)); - const commonFieldsAtStart = []; - const commonFieldsAtEnd = []; - const groups = getSources(alert) as Array<{ field: string; value: string }>; - - useEffect(() => { - setTimeRange(getPaddedAlertTimeRange(alertStart!, alertEnd)); - }, [alertStart, alertEnd]); - - if (groups && groups.length > 0) { - commonFieldsAtStart.push({ - label: i18n.translate('xpack.observability.alertDetails.alertSummaryField.source', { - defaultMessage: 'Source', - }), - value: ( - - ), - }); - } - - if (tags && tags.length > 0) { - commonFieldsAtEnd.push({ - label: i18n.translate('xpack.observability.alertDetails.alertSummaryField.tags', { - defaultMessage: 'Tags', - }), - value: , - }); - } - - commonFieldsAtEnd.push({ - label: i18n.translate('xpack.observability.alertDetails.alertSummaryField.rule', { - defaultMessage: 'Rule', - }), - value: ( - - {ruleName} - - ), - }); - - const alertSummary = [ - ...commonFieldsAtStart, - ...(alertSummaryFields ?? []), - ...commonFieldsAtEnd, - ]; - - return ( -
- {alertSummary && alertSummary.length > 0 && ( - <> - - {alertSummary.map((field, idx) => { - return ( - - {field.label} - {field.value} - - ); - })} - - - - )} -
- ); -} - -// eslint-disable-next-line import/no-default-export -export default AlertSummary; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/index.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/index.tsx index 8af473faab59d..bf032ac31c127 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/index.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/index.tsx @@ -6,14 +6,14 @@ */ import React, { lazy, Suspense } from 'react'; -import type { AlertSummaryProps } from './alert_summary'; +import type { SourceBarProps } from './source_bar'; -const AlertSummaryLazy = lazy(() => import('./alert_summary')); +const SourceBarLazy = lazy(() => import('./source_bar')); -export function AlertSummary(props: AlertSummaryProps) { +export function SourceBar(props: SourceBarProps) { return ( - + ); } diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title.stories.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title.stories.tsx deleted file mode 100644 index 30ca4b6a108cb..0000000000000 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { ComponentStory } from '@storybook/react'; -import { EuiPageTemplate } from '@elastic/eui'; - -import { PageTitleContent as Component, PageTitleContentProps } from './page_title_content'; -import { alert } from '../mock/alert'; - -export default { - component: Component, - title: 'app/AlertDetails/PageTitleContent', - alert, -}; - -const Template: ComponentStory = (props: PageTitleContentProps) => ( - -); - -const TemplateWithPageTemplate: ComponentStory = ( - props: PageTitleContentProps -) => ( - - } bottomBorder={false} /> - -); - -const defaultProps = { - alert, -}; - -export const PageTitleContent = Template.bind({}); -PageTitleContent.args = defaultProps; - -export const PageTitleUsedWithinPageTemplate = TemplateWithPageTemplate.bind({}); -PageTitleUsedWithinPageTemplate.args = { - ...defaultProps, -}; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.test.tsx deleted file mode 100644 index d65c4f84c99dd..0000000000000 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { render } from '@testing-library/react'; -import { __IntlProvider as IntlProvider } from '@kbn/i18n-react'; -import { - AlertStatus, - ALERT_STATUS, - ALERT_STATUS_ACTIVE, - ALERT_STATUS_RECOVERED, - ALERT_STATUS_UNTRACKED, -} from '@kbn/rule-data-utils'; -import { PageTitleContent, PageTitleContentProps } from './page_title_content'; -import { alert } from '../mock/alert'; - -describe('Page Title Content', () => { - const defaultProps = { - alert, - alertStatus: ALERT_STATUS_ACTIVE as AlertStatus, - dataTestSubj: 'ruleTypeId', - }; - - const renderComp = (props: PageTitleContentProps) => { - return render( - - - - ); - }; - - it('should display an active badge when alert is active', async () => { - const { getByText } = renderComp(defaultProps); - expect(getByText('Active')).toBeTruthy(); - }); - - it('should display a recovered badge when alert is recovered', async () => { - const updatedProps = { - alert: { - ...defaultProps.alert, - fields: { - ...defaultProps.alert.fields, - [ALERT_STATUS]: ALERT_STATUS_RECOVERED, - }, - }, - alertStatus: ALERT_STATUS_RECOVERED as AlertStatus, - dataTestSubj: defaultProps.dataTestSubj, - }; - - const { getByText } = renderComp({ ...updatedProps }); - expect(getByText('Recovered')).toBeTruthy(); - }); - - it('should display an untracked badge when alert is untracked', async () => { - const updatedProps = { - alert: { - ...defaultProps.alert, - fields: { - ...defaultProps.alert.fields, - [ALERT_STATUS]: ALERT_STATUS_UNTRACKED, - }, - }, - alertStatus: ALERT_STATUS_UNTRACKED as AlertStatus, - dataTestSubj: defaultProps.dataTestSubj, - }; - - const { getByText } = renderComp({ ...updatedProps }); - expect(getByText('Untracked')).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.test.tsx new file mode 100644 index 0000000000000..6a68d77175fef --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiLink } from '@elastic/eui'; +import React from 'react'; +import { ALERT_GROUP } from '@kbn/rule-data-utils'; +import { render } from '../../../utils/test_helper'; +import { alertWithGroupsAndTags } from '../mock/alert'; +import { useKibana } from '../../../utils/kibana_react'; +import { kibanaStartMock } from '../../../utils/kibana_react.mock'; +import { Group } from '../../../../common/typings'; +import { SourceBar } from './source_bar'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), +})); + +jest.mock('../../../utils/kibana_react'); + +const useKibanaMock = useKibana as jest.Mock; +const mockKibana = () => { + useKibanaMock.mockReturnValue({ + services: { + ...kibanaStartMock.startContract().services, + http: { + basePath: { + prepend: jest.fn(), + }, + }, + }, + }); +}; + +describe('Source bar', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockKibana(); + }); + + it('should show alert data', async () => { + const sourceBar = render(); + + const groups = alertWithGroupsAndTags.fields[ALERT_GROUP] as Group[]; + + expect(sourceBar.queryByText('Source')).toBeInTheDocument(); + expect(sourceBar.queryByText(groups[0].field, { exact: false })).toBeInTheDocument(); + expect(sourceBar.queryByText(groups[0].value)).toBeInTheDocument(); + expect(sourceBar.queryByText(groups[1].field, { exact: false })).toBeInTheDocument(); + expect(sourceBar.queryByText(groups[1].value)).toBeInTheDocument(); + }); + + it('Should show passed sources', async () => { + const sources = [ + { label: 'MyLabel', value: 'MyValue' }, + { label: 'SLO', value: }, + ]; + const sourceBar = render(); + + expect(sourceBar.queryByText('Source')).toBeInTheDocument(); + expect(sourceBar.queryByText(sources[0].label, { exact: false })).toBeInTheDocument(); + expect(sourceBar.queryByText(sources[0].value as string, { exact: false })).toBeInTheDocument(); + expect(sourceBar.queryByText(sources[1].label, { exact: false })).toBeInTheDocument(); + expect(sourceBar.queryByTestId('SourceSloLink')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.tsx new file mode 100644 index 0000000000000..be47a3f9ec3e9 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/source_bar.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FormattedMessage } from '@kbn/i18n-react'; +import React, { useEffect, useState } from 'react'; +import { EuiFlexGroup, EuiTitle, EuiPanel, EuiFlexItem, EuiText } from '@elastic/eui'; +import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util'; +import { ALERT_START, ALERT_END } from '@kbn/rule-data-utils'; +import { TimeRange } from '@kbn/es-query'; +import { AlertDetailsSource } from '../types'; +import { TopAlert } from '../../..'; +import { Groups } from '../../../components/alert_sources/groups'; +import { getSources } from '../../../components/alert_sources/get_sources'; + +export interface SourceBarProps { + alert: TopAlert; + sources?: AlertDetailsSource[]; +} + +export function SourceBar({ alert, sources = [] }: SourceBarProps) { + const [timeRange, setTimeRange] = useState({ from: 'now-15m', to: 'now' }); + + const alertStart = alert.fields[ALERT_START]; + const alertEnd = alert.fields[ALERT_END]; + const groups = getSources(alert); + + useEffect(() => { + setTimeRange(getPaddedAlertTimeRange(alertStart!, alertEnd)); + }, [alertStart, alertEnd]); + + return ( + <> + {groups && groups.length > 0 && ( + + + +
+ +
+
+ + {sources.map((field, idx) => { + return ( + + + {field.label}: {field.value} + + + ); + })} +
+
+ )} + + ); +} + +// eslint-disable-next-line import/no-default-export +export default SourceBar; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.stories.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.stories.tsx new file mode 100644 index 0000000000000..0c0eada41e1e7 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.stories.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ComponentStory } from '@storybook/react'; +import { I18nProvider } from '@kbn/i18n-react'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; + +import { StatusBar as Component, StatusBarProps } from './status_bar'; +import { alert } from '../mock/alert'; + +export default { + component: Component, + title: 'app/AlertDetails/StatusBar', + alert, +}; + +const Template: ComponentStory = (props: StatusBarProps) => ( + + + + + +); + +const defaultProps = { + alert, +}; + +export const StatusBar = Template.bind({}); +StatusBar.args = defaultProps; + +const services = { + http: { + basePath: { + prepend: () => 'http://test', + }, + }, +}; diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.test.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.test.tsx new file mode 100644 index 0000000000000..fb92597afdc00 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + ALERT_RULE_NAME, + ALERT_STATUS, + ALERT_STATUS_RECOVERED, + ALERT_STATUS_UNTRACKED, + AlertStatus, +} from '@kbn/rule-data-utils'; +import { render } from '../../../utils/test_helper'; +import { alertWithGroupsAndTags } from '../mock/alert'; +import { useKibana } from '../../../utils/kibana_react'; +import { kibanaStartMock } from '../../../utils/kibana_react.mock'; +import { StatusBar, StatusBarProps } from './status_bar'; + +jest.mock('../../../utils/kibana_react'); + +const useKibanaMock = useKibana as jest.Mock; +const mockKibana = () => { + useKibanaMock.mockReturnValue({ + services: { + ...kibanaStartMock.startContract().services, + http: { + basePath: { + prepend: jest.fn(), + }, + }, + }, + }); +}; + +describe('Source bar', () => { + const renderComponent = (props: StatusBarProps) => { + return render(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockKibana(); + }); + + it('should show alert data', async () => { + const statusBar = renderComponent({ + alert: alertWithGroupsAndTags, + alertStatus: alertWithGroupsAndTags.fields[ALERT_STATUS] as AlertStatus, + }); + + expect( + statusBar.queryByText(alertWithGroupsAndTags.fields[ALERT_RULE_NAME]) + ).toBeInTheDocument(); + expect(statusBar.getByText('Active')).toBeTruthy(); + }); + + it('should display a recovered badge when alert is recovered', async () => { + const updatedProps = { + alert: { + ...alertWithGroupsAndTags, + fields: { + ...alertWithGroupsAndTags.fields, + [ALERT_STATUS]: ALERT_STATUS_RECOVERED, + }, + }, + alertStatus: ALERT_STATUS_RECOVERED as AlertStatus, + }; + + const { getByText } = renderComponent({ ...updatedProps }); + expect(getByText('Recovered')).toBeTruthy(); + }); + + it('should display an untracked badge when alert is untracked', async () => { + const updatedProps = { + alert: { + ...alertWithGroupsAndTags, + fields: { + ...alertWithGroupsAndTags.fields, + [ALERT_STATUS]: ALERT_STATUS_UNTRACKED, + }, + }, + alertStatus: ALERT_STATUS_UNTRACKED as AlertStatus, + }; + + const { getByText } = renderComponent({ ...updatedProps }); + expect(getByText('Untracked')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.tsx b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.tsx similarity index 55% rename from x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.tsx rename to x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.tsx index 11fc6d0a476bb..eee2db8e98d52 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/page_title_content.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/alert_details/components/status_bar.tsx @@ -7,30 +7,51 @@ import React from 'react'; import moment from 'moment'; -import { EuiFlexGroup, EuiFlexItem, EuiText, useEuiTheme } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, useEuiTheme, EuiToolTip } from '@elastic/eui'; import { AlertLifecycleStatusBadge } from '@kbn/alerts-ui-shared'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import { AlertStatus, ALERT_DURATION, ALERT_FLAPPING, TIMESTAMP } from '@kbn/rule-data-utils'; +import { + AlertStatus, + ALERT_DURATION, + ALERT_FLAPPING, + TIMESTAMP, + TAGS, + ALERT_RULE_NAME, + ALERT_RULE_UUID, +} from '@kbn/rule-data-utils'; import { css } from '@emotion/react'; +import { TagsList } from '@kbn/observability-shared-plugin/public'; +import { useKibana } from '../../../utils/kibana_react'; +import { paths } from '../../../../common/locators/paths'; import { asDuration } from '../../../../common/utils/formatters'; import { TopAlert } from '../../../typings/alerts'; -export interface PageTitleContentProps { +export interface StatusBarProps { alert: TopAlert | null; alertStatus?: AlertStatus; - dataTestSubj: string; } -export function PageTitleContent({ alert, alertStatus, dataTestSubj }: PageTitleContentProps) { +export function StatusBar({ alert, alertStatus }: StatusBarProps) { + const { http } = useKibana().services; const { euiTheme } = useEuiTheme(); + const tags = alert?.fields[TAGS]; + const ruleName = alert?.fields[ALERT_RULE_NAME]; + const ruleId = alert?.fields[ALERT_RULE_UUID]; + const ruleLink = ruleId ? http.basePath.prepend(paths.observability.ruleDetails(ruleId)) : ''; if (!alert) { return null; } return ( - + {alertStatus && ( + + + + + + + :  + + + + + {ruleName} + + + + + + - + - + >; +} diff --git a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx index 57c92ea7ebc8d..9eb7c83b03fc0 100644 --- a/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx +++ b/x-pack/plugins/observability_solution/observability_onboarding/public/application/quickstart_flows/otel_logs/index.tsx @@ -556,7 +556,7 @@ rm ./otel.yml && cp ./otel_samples/platformlogs_hostmetrics.yml ./otel.yml && mk id: 'mac', name: 'Mac', firstStepTitle: HOST_COMMAND, - content: `arch=$(if [[ $(arch) == "arm64" ]]; then echo "aarch64"; else echo $(arch); fi) + content: `arch=$(if [[ $(uname -m) == "arm64" ]]; then echo "aarch64"; else echo $(uname -m); fi) curl --output elastic-distro-${agentVersion}-darwin-$arch.tar.gz --url https://${AGENT_CDN_BASE_URL}/elastic-agent-${agentVersion}-darwin-$arch.tar.gz --proto '=https' --tlsv1.2 -fOL && mkdir -p "elastic-distro-${agentVersion}-darwin-$arch" && tar -xvf elastic-distro-${agentVersion}-darwin-$arch.tar.gz -C "elastic-distro-${agentVersion}-darwin-$arch" --strip-components=1 && cd elastic-distro-${agentVersion}-darwin-$arch diff --git a/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts b/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts index afaf78ef1aa9b..741e9b6b0e2d3 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/field_names/elasticsearch.ts @@ -145,3 +145,12 @@ export const PROFILE_ALLOC_OBJECTS = 'profile.alloc_objects.count'; export const PROFILE_ALLOC_SPACE = 'profile.alloc_space.bytes'; export const PROFILE_INUSE_OBJECTS = 'profile.inuse_objects.count'; export const PROFILE_INUSE_SPACE = 'profile.inuse_space.bytes'; + +export const ENTITY = 'entity'; +export const ENTITY_ID = 'entity.id'; +export const ENTITY_TYPE = 'entity.type'; +export const ENTITY_LAST_SEEN = 'entity.lastSeenTimestamp'; +export const ENTITY_FIRST_SEEN = 'entity.firstSeenTimestamp'; +export const ENTITY_DISPLAY_NAME = 'entity.displayName'; +export const ENTITY_DEFINITION_ID = 'entity.definitionId'; +export const SOURCE_DATA_STREAM_TYPE = 'source_data_stream.type'; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/index.ts index e14bbb4139176..a33c0aa99d1e2 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/index.ts @@ -128,6 +128,14 @@ export { PROFILE_ALLOC_SPACE, PROFILE_INUSE_OBJECTS, PROFILE_INUSE_SPACE, + ENTITY, + ENTITY_DEFINITION_ID, + ENTITY_DISPLAY_NAME, + ENTITY_FIRST_SEEN, + ENTITY_ID, + ENTITY_LAST_SEEN, + ENTITY_TYPE, + SOURCE_DATA_STREAM_TYPE, } from './field_names/elasticsearch'; export { diff --git a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx index be83b74a0cc19..44ab29e77e1bd 100644 --- a/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx +++ b/x-pack/plugins/observability_solution/slo/public/components/slo/burn_rate/alert_details/alert_details_app_section.tsx @@ -4,28 +4,24 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +import React, { useEffect } from 'react'; import { EuiFlexGroup, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { AlertSummaryField } from '@kbn/observability-plugin/public'; -import React, { useEffect } from 'react'; -import { useFetchSloDetails } from '../../../../hooks/use_fetch_slo_details'; +import { AlertDetailsAppSectionProps } from '@kbn/observability-plugin/public'; import { useKibana } from '../../../../utils/kibana_react'; +import { useFetchSloDetails } from '../../../../hooks/use_fetch_slo_details'; import { CustomAlertDetailsPanel } from './components/custom_panels/custom_panels'; import { ErrorRatePanel } from './components/error_rate/error_rate_panel'; import { BurnRateAlert, BurnRateRule } from './types'; -interface AppSectionProps { +interface AppSectionProps extends AlertDetailsAppSectionProps { alert: BurnRateAlert; rule: BurnRateRule; - setAlertSummaryFields: React.Dispatch>; } // eslint-disable-next-line import/no-default-export -export default function AlertDetailsAppSection({ - alert, - rule, - setAlertSummaryFields, -}: AppSectionProps) { +export default function AlertDetailsAppSection({ alert, rule, setSources }: AppSectionProps) { const { services: { http: { basePath }, @@ -51,8 +47,8 @@ export default function AlertDetailsAppSection({ }, ]; - setAlertSummaryFields(fields); - }, [alertLink, rule, setAlertSummaryFields, basePath, slo, instanceId]); + setSources(fields); + }, [alertLink, rule, setSources, basePath, slo, instanceId]); return ( diff --git a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/remove_kql_variables.ts b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/remove_kql_variables.ts index 18ad035debf08..f7730f650d4ee 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/remove_kql_variables.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/remove_kql_variables.ts @@ -13,7 +13,7 @@ export const operators = ['and', 'or', 'not']; export const removeKqlVariablesUsingRegex = (expression: string) => { const myRegexp = - /(\s+)*(and|or|not){0,1}(\s+)*([\w\.\-\[\]]+)\s*:\s*"(\$[\w\.\-\(\)\[\]]+\$)"(\s+)*(and|or|not){0,1}(\s+)*/g; + /(\s*)(and|or|not){0,1}(\s*)([\w\.\-\[\]]+)\s*:\s*"(\$[\w\.\-\(\)\[\]]+\$)"(\s*)(and|or|not){0,1}(\s*)/g; return expression.replace(myRegexp, replacer); }; diff --git a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/replace_kql_commas_with_or.ts b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/replace_kql_commas_with_or.ts index 1f96615811fc7..2179495ad7205 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/replace_kql_commas_with_or.ts +++ b/x-pack/plugins/security_solution/public/common/components/ml/conditional_links/replace_kql_commas_with_or.ts @@ -22,7 +22,7 @@ export const replacement = (match: string, p1: string, p2: string) => { }; export const replaceKqlCommasWithOrUsingRegex = (expression: string) => { - const myRegexp = /([\w\.\-\[\]]+)\s*:\s*"(([\w\.\-\(\)\[\]]+,[\w\.\-\(\)\[\]]+){1,})"/g; + const myRegexp = /([\w\.\-\[\]]+)\s*:\s*"(([\w\.\-\(\)\[\]]+)(,[\w\.\-\(\)\[\]]+){1,})"/g; return expression.replace(myRegexp, replacement); }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/schema.tsx index bdf14f4b6fd4a..5c8d8d89c46d0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/components/step_define_rule/schema.tsx @@ -643,6 +643,35 @@ export const schema: FormSchema = { defaultMessage: "New terms rules only alert if terms don't appear in historical data.", } ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ path, formData }] = args; + const needsValidation = isNewTermsRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + const filterTimeVal = formData.historyWindowSize.match(/\d+/g); + + if (filterTimeVal <= 0) { + return { + code: 'ERR_MIN_LENGTH', + path, + message: i18n.translate( + 'xpack.securitySolution.detectionEngine.validations.stepDefineRule.historyWindowSize.errMin', + { + defaultMessage: 'History window size must be greater than 0.', + } + ), + }; + } + }, + }, + ], }, groupByFields: { type: FIELD_TYPES.COMBO_BOX, diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/metering.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/metering.cy.ts index d354531affd35..faecf1f71f148 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/metering.cy.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/serverless/metering.cy.ts @@ -25,7 +25,7 @@ describe( ftrConfig: { kbnServerArgs: [ `--xpack.securitySolutionServerless.usageReportingTaskInterval=1m`, - `--xpack.securitySolutionServerless.usageReportingApiUrl=https://localhost:3623`, + `--xpack.securitySolutionServerless.usageApi.url=https://localhost:3623`, ], }, }, diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/project_handler/cloud_project_handler.ts b/x-pack/plugins/security_solution/scripts/run_cypress/project_handler/cloud_project_handler.ts index dd1e2c8762909..5ae476c17580e 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/project_handler/cloud_project_handler.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/project_handler/cloud_project_handler.ts @@ -40,8 +40,12 @@ export class CloudHandler extends ProjectHandler { body.product_types = productTypes; } - if (process.env.KIBANA_MKI_IMAGE_COMMIT || commit) { - const override = commit ? commit : process.env.KIBANA_MKI_IMAGE_COMMIT; + // The qualityGate variable has been added here to ensure that when the quality gate runs, there will be + // no kibana image override. The tests will be executed against the commit which is already promoted to QA. + const qualityGate = + process.env.KIBANA_MKI_QUALITY_GATE && process.env.KIBANA_MKI_QUALITY_GATE === '1'; + const override = commit ?? process.env.KIBANA_MKI_IMAGE_COMMIT; + if (override && !qualityGate) { const kibanaOverrideImage = `${override?.substring(0, 12)}`; this.log.info(`Kibana Image Commit under test: ${process.env.KIBANA_MKI_IMAGE_COMMIT}!`); this.log.info( diff --git a/x-pack/plugins/security_solution/scripts/run_cypress/project_handler/proxy_project_handler.ts b/x-pack/plugins/security_solution/scripts/run_cypress/project_handler/proxy_project_handler.ts index b438849d85b4c..dfc97e9a422d8 100644 --- a/x-pack/plugins/security_solution/scripts/run_cypress/project_handler/proxy_project_handler.ts +++ b/x-pack/plugins/security_solution/scripts/run_cypress/project_handler/proxy_project_handler.ts @@ -40,8 +40,12 @@ export class ProxyHandler extends ProjectHandler { body.product_types = productTypes; } - if (process.env.KIBANA_MKI_IMAGE_COMMIT || commit) { - const override = commit ? commit : process.env.KIBANA_MKI_IMAGE_COMMIT; + // The qualityGate variable has been added here to ensure that when the quality gate runs, there will be + // no kibana image override. The tests will be executed against the commit which is already promoted to QA. + const qualityGate = + process.env.KIBANA_MKI_QUALITY_GATE && process.env.KIBANA_MKI_QUALITY_GATE === '1'; + const override = commit ?? process.env.KIBANA_MKI_IMAGE_COMMIT; + if (override && !qualityGate) { const kibanaOverrideImage = `${override?.substring(0, 12)}`; this.log.info(`Kibana Image Commit under test: ${process.env.KIBANA_MKI_IMAGE_COMMIT}!`); this.log.info( diff --git a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/esql_language_knowledge_base_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/esql_language_knowledge_base_tool.ts index 908d69ab0802e..6bf116c28719a 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/esql_language_knowledge_base_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/esql_language_knowledge_base_tool.ts @@ -17,7 +17,7 @@ import { APP_UI_ID } from '../../../../common'; const toolDetails = { description: - 'Call this for knowledge on how to build an ESQL query, or answer questions about the ES|QL query language. Input must always be the query on a single line, with no other text. Your answer will be parsed as JSON, so never use quotes within the output and instead use backticks. Do not add any additional text to describe your output.', + 'Call this for knowledge on how to build an ESQL query, or answer questions about the ES|QL query language. Input must always be the user query on a single line, with no other text. Your answer will be parsed as JSON, so never use quotes within the output and instead use backticks. Do not add any additional text to describe your output.', id: 'esql-knowledge-base-tool', name: 'ESQLKnowledgeBaseTool', }; diff --git a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/nl_to_esql_tool.ts b/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/nl_to_esql_tool.ts index c313d877cc55b..a26d16607ac46 100644 --- a/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/nl_to_esql_tool.ts +++ b/x-pack/plugins/security_solution/server/assistant/tools/esql_language_knowledge_base/nl_to_esql_tool.ts @@ -20,16 +20,11 @@ const toolDetails = { id: 'nl-to-esql-tool', name: TOOL_NAME, description: `You MUST use the "${TOOL_NAME}" function when the user wants to: - - run any arbitrary query - breakdown or filter ES|QL queries that are displayed on the current page - convert queries from another language to ES|QL - asks general questions about ES|QL - DO NOT UNDER ANY CIRCUMSTANCES generate ES|QL queries or explain anything about the ES|QL query language yourself. - DO NOT UNDER ANY CIRCUMSTANCES try to correct an ES|QL query yourself - always use the "${TOOL_NAME}" function for this. - - Even if the "${TOOL_NAME}" function was used before that, follow it up with the "${TOOL_NAME}" function. If a query fails, do not attempt to correct it yourself. Again you should call the "${TOOL_NAME}" function, - even if it has been called before.`, + ALWAYS use this tool to generate ES|QL queries or explain anything about the ES|QL query language rather than coming up with your own answer.`, }; export const NL_TO_ESQL_TOOL: AssistantTool = { diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_migration_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_migration_client.ts index b9af8a133ec39..b87a172b4c746 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_migration_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/asset_criticality/asset_criticality_migration_client.ts @@ -77,7 +77,7 @@ export class AssetCriticalityEcsMigrationClient { conflicts: 'proceed', ignore_unavailable: true, allow_no_indices: true, - scroll_size: 1000, + scroll_size: 10000, body: { query: ECS_MAPPINGS_MIGRATION_QUERY, script: { @@ -87,6 +87,7 @@ export class AssetCriticalityEcsMigrationClient { }, }, { + requestTimeout: '5m', retryOnTimeout: true, maxRetries: 2, signal: abortSignal, diff --git a/x-pack/plugins/security_solution_serverless/server/config.ts b/x-pack/plugins/security_solution_serverless/server/config.ts index 4b9700aaca6b2..96e743a59b425 100644 --- a/x-pack/plugins/security_solution_serverless/server/config.ts +++ b/x-pack/plugins/security_solution_serverless/server/config.ts @@ -5,14 +5,31 @@ * 2.0. */ -import { schema, type TypeOf } from '@kbn/config-schema'; +import type { TypeOf } from '@kbn/config-schema'; import type { PluginConfigDescriptor, PluginInitializerContext } from '@kbn/core/server'; import type { SecuritySolutionPluginSetup } from '@kbn/security-solution-plugin/server/plugin_contract'; -import { USAGE_SERVICE_USAGE_URL } from './constants'; -import { productTypes } from '../common/config'; + +import { schema } from '@kbn/config-schema'; + import type { ExperimentalFeatures } from '../common/experimental_features'; + +import { productTypes } from '../common/config'; import { parseExperimentalConfigValue } from '../common/experimental_features'; +const usageApiConfig = schema.maybe( + schema.object({ + enabled: schema.maybe(schema.boolean()), + url: schema.string(), + tls: schema.maybe( + schema.object({ + certificate: schema.string(), + key: schema.string(), + ca: schema.string(), + }) + ), + }) +); + export const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: false }), productTypes, @@ -33,10 +50,6 @@ export const configSchema = schema.object({ */ usageReportingTaskTimeout: schema.string({ defaultValue: '1m' }), - /** - * Usage Reporting: the URL to send usage data to - */ - usageReportingApiUrl: schema.string({ defaultValue: USAGE_SERVICE_USAGE_URL }), /** * For internal use. A list of string values (comma delimited) that will enable experimental * type of functionality that is not yet released. Valid values for this settings need to @@ -52,6 +65,8 @@ export const configSchema = schema.object({ enableExperimental: schema.arrayOf(schema.string(), { defaultValue: () => [], }), + + usageApi: usageApiConfig, }); export type ServerlessSecuritySchema = TypeOf; diff --git a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts index 56c971e5d1df8..66307e8f8a693 100644 --- a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts +++ b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.test.ts @@ -150,7 +150,7 @@ describe('SecurityUsageReportingTask', () => { productTypes: [ { product_line: ProductLine.endpoint, product_tier: ProductTier.complete }, ], - usageReportingApiUrl: USAGE_SERVICE_USAGE_URL, + usageApi: { url: USAGE_SERVICE_USAGE_URL }, } as ServerlessSecurityConfig, }); mockTask = new SecurityUsageReportingTask(taskArgs); @@ -227,7 +227,7 @@ describe('SecurityUsageReportingTask', () => { }); taskArgs = buildTaskArgs({ config: { - usageReportingApiUrl: USAGE_SERVICE_USAGE_URL, + usageApi: { url: USAGE_SERVICE_USAGE_URL }, } as ServerlessSecurityConfig, }); mockTask = new SecurityUsageReportingTask(taskArgs); diff --git a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts index 8b965cb597153..83ef25a849f2d 100644 --- a/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts +++ b/x-pack/plugins/security_solution_serverless/server/task_manager/usage_reporting_task.ts @@ -165,7 +165,7 @@ export class SecurityUsageReportingTask { usageReportResponse = await usageReportingService.reportUsage( usageRecords, - this.config.usageReportingApiUrl + this.config.usageApi?.url ); if (!usageReportResponse.ok) { diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx index 9150f0c211adb..a7cc54820774d 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.test.tsx @@ -160,6 +160,32 @@ describe('PrivilegesRolesForm', () => { expect(screen.getByTestId('space-assign-role-create-roles-privilege-button')).toBeDisabled(); }); + it('renders with the assign roles button disabled when no base privileges or feature privileges are selected', async () => { + getRolesSpy.mockResolvedValue([]); + getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); + + const roles: Role[] = [ + createRole('test_role_1', [{ base: [], feature: {}, spaces: [space.id] }]), + ]; + + renderPrivilegeRolesForm({ + preSelectedRoles: roles, + }); + + await waitFor(() => null); + + expect(screen.getByTestId(`${FEATURE_PRIVILEGES_READ}-privilege-button`)).toHaveAttribute( + 'aria-pressed', + String(false) + ); + + expect( + screen.getByTestId('space-assign-role-privilege-customization-form') + ).toBeInTheDocument(); + + expect(screen.getByTestId('space-update-role-create-roles-privilege-button')).toBeDisabled(); + }); + it('preselects the privilege of the selected role when one is provided', async () => { getRolesSpy.mockResolvedValue([]); getAllKibanaPrivilegeSpy.mockResolvedValue(createRawKibanaPrivileges(kibanaFeatures)); diff --git a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx index 276efb7f92526..23a7383a01a06 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx +++ b/x-pack/plugins/spaces/public/management/edit_space/roles/component/space_assign_role_privilege_form.tsx @@ -543,11 +543,32 @@ export const PrivilegesRolesForm: FC = (props) => { ); }; + const canSave = useCallback(() => { + if (selectedRoles.length === 0) { + return false; + } + + const form = roleCustomizationAnchor.value.kibana[roleCustomizationAnchor.privilegeIndex] ?? {}; + const formBase = form.base ?? []; + const formFeature = form.feature ?? {}; + + // ensure that the form has base privileges or has selected features that are valid + if ( + formBase.length === 0 && + (Object.keys(formFeature).length === 0 || + Object.values(formFeature).every((privileges) => privileges.length === 0)) + ) { + return false; + } + + return true; + }, [selectedRoles, roleCustomizationAnchor]); + const getSaveButton = useCallback(() => { return ( assignRolesToSpace()} data-test-subj={`space-${ @@ -563,7 +584,7 @@ export const PrivilegesRolesForm: FC = (props) => { })} ); - }, [assignRolesToSpace, assigningToRole, selectedRoles.length]); + }, [assignRolesToSpace, assigningToRole, canSave]); return ( diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts index f53bb02787f8d..364afdcaba66a 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts @@ -31,6 +31,35 @@ const createMockConfig = ( return ConfigSchema.validate(mockConfig, { serverless: !mockConfig.allowFeatureVisibility }); }; +const features = [ + { + id: 'feature_1', + name: 'Feature 1', + app: [], + category: { id: 'enterpriseSearch' }, + scope: ['spaces', 'security'], + }, + { + id: 'feature_2', + name: 'Feature 2', + app: ['feature2'], + scope: ['spaces', 'security'], + catalogue: ['feature2Entry'], + category: { id: 'observability' }, + }, + { + id: 'feature_3', + name: 'Feature 3', + app: ['feature3_app'], + scope: ['spaces', 'security'], + catalogue: ['feature3Entry'], + category: { id: 'securitySolution' }, + }, +] as unknown as KibanaFeature[]; +const featuresStart = featuresPluginMock.createStart(); + +featuresStart.getKibanaFeatures.mockReturnValue([...features]); + describe('#getAll', () => { const savedObjects: Array> = [ { @@ -84,7 +113,7 @@ describe('#getAll', () => { color: '#FFFFFF', initials: 'FB', imageUrl: 'go-bots/predates/transformers', - disabledFeatures: [], + disabledFeatures: ['feature_2', 'feature_3'], // Added dynamically because solution is 'es' solution: 'es', _reserved: true, }, @@ -117,7 +146,7 @@ describe('#getAll', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const actualSpaces = await client.getAll(); @@ -145,13 +174,16 @@ describe('#getAll', () => { mockCallWithRequestRepository, [], 'serverless', - featuresPluginMock.createStart() + featuresStart ); const [actualSpace] = await client.getAll(); - const [{ solution, ...expectedSpace }] = expectedSpaces; + const [{ solution, disabledFeatures, ...expectedSpace }] = expectedSpaces; expect(actualSpace.solution).toBeUndefined(); - expect(actualSpace).toEqual(expectedSpace); + expect(actualSpace).toEqual({ + ...expectedSpace, + disabledFeatures: [], // And the disabledFeatures is not dynamically added + }); expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ type: 'space', page: 1, @@ -170,7 +202,7 @@ describe('#getAll', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); await expect( client.getAll({ purpose: 'invalid_purpose' as GetAllSpacesPurpose }) @@ -218,7 +250,7 @@ describe('#get', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const id = savedObject.id; const actualSpace = await client.get(id); @@ -242,7 +274,7 @@ describe('#get', () => { mockCallWithRequestRepository, [], 'serverless', - featuresPluginMock.createStart() + featuresStart ); const id = savedObject.id; const actualSpace = await client.get(id); @@ -266,12 +298,16 @@ describe('#get', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const id = savedObject.id; const actualSpace = await client.get(id); - expect(actualSpace).toEqual({ ...expectedSpace, solution: 'es' }); + expect(actualSpace).toEqual({ + ...expectedSpace, + solution: 'es', + disabledFeatures: ['feature_2', 'feature_3'], // Added dynamically because solution is 'es' + }); }); }); @@ -312,7 +348,10 @@ describe('#create', () => { const maxSpaces = 5; const mockDebugLogger = createMockDebugLogger(); const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); - mockCallWithRequestRepository.create.mockResolvedValue(savedObject); + mockCallWithRequestRepository.create.mockResolvedValue({ + ...savedObject, + attributes: { ...(savedObject.attributes as object), solution: 'es' }, + }); mockCallWithRequestRepository.find.mockResolvedValue({ total: maxSpaces - 1, } as any); @@ -330,20 +369,28 @@ describe('#create', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); - const actualSpace = await client.create(spaceToCreate); + const actualSpace = await client.create({ ...spaceToCreate, solution: 'es' }); - expect(actualSpace).toEqual(expectedReturnedSpace); + expect(actualSpace).toEqual({ + ...expectedReturnedSpace, + solution: 'es', + disabledFeatures: ['feature_2', 'feature_3'], // Added dynamically because solution is 'es' + }); expect(mockCallWithRequestRepository.find).toHaveBeenCalledWith({ type: 'space', page: 1, perPage: 0, }); - expect(mockCallWithRequestRepository.create).toHaveBeenCalledWith('space', attributes, { - id, - }); + expect(mockCallWithRequestRepository.create).toHaveBeenCalledWith( + 'space', + { ...attributes, solution: 'es' }, + { + id, + } + ); }); test(`throws bad request when creating space with disabled features`, async () => { @@ -355,7 +402,7 @@ describe('#create', () => { mockCallWithRequestRepository.find.mockResolvedValue({ total: maxSpaces - 1, } as any); - const featuresMock = featuresPluginMock.createStart(); + const featuresMock = featuresStart; featuresMock.getKibanaFeatures.mockReturnValue([ new KibanaFeature({ @@ -422,7 +469,7 @@ describe('#create', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); await expect(client.create(spaceToCreate)).rejects.toThrowErrorMatchingInlineSnapshot( @@ -459,7 +506,7 @@ describe('#create', () => { mockCallWithRequestRepository, [], 'serverless', - featuresPluginMock.createStart() + featuresStart ); await expect( @@ -507,7 +554,7 @@ describe('#create', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const actualSpace = await client.create({ ...spaceToCreate, solution: 'es' }); @@ -551,7 +598,7 @@ describe('#create', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const actualSpace = await client.create(spaceToCreate); @@ -589,7 +636,7 @@ describe('#create', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); await expect( @@ -630,7 +677,7 @@ describe('#create', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); await expect( @@ -687,7 +734,11 @@ describe('#update', () => { const mockDebugLogger = createMockDebugLogger(); const mockConfig = createMockConfig(); const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); - mockCallWithRequestRepository.get.mockResolvedValue(savedObject); + mockCallWithRequestRepository.get.mockResolvedValueOnce({ + ...savedObject, + attributes: { ...(savedObject.attributes as object), solution: 'es' }, + }); + featuresStart.getKibanaFeatures.mockReturnValue([...features]); const client = new SpacesClient( mockDebugLogger, @@ -695,13 +746,20 @@ describe('#update', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const id = savedObject.id; - const actualSpace = await client.update(id, spaceToUpdate); + const actualSpace = await client.update(id, { ...spaceToUpdate, solution: 'es' }); - expect(actualSpace).toEqual(expectedReturnedSpace); - expect(mockCallWithRequestRepository.update).toHaveBeenCalledWith('space', id, attributes); + expect(actualSpace).toEqual({ + ...expectedReturnedSpace, + solution: 'es', + disabledFeatures: ['feature_2', 'feature_3'], // Added dynamically because solution is 'es' + }); + expect(mockCallWithRequestRepository.update).toHaveBeenCalledWith('space', id, { + ...attributes, + solution: 'es', + }); expect(mockCallWithRequestRepository.get).toHaveBeenCalledWith('space', id); }); @@ -710,7 +768,7 @@ describe('#update', () => { const mockConfig = createMockConfig(); const mockCallWithRequestRepository = savedObjectsRepositoryMock.create(); mockCallWithRequestRepository.get.mockResolvedValue(savedObject); - const featuresMock = featuresPluginMock.createStart(); + const featuresMock = featuresStart; featuresMock.getKibanaFeatures.mockReturnValue([ new KibanaFeature({ @@ -767,7 +825,7 @@ describe('#update', () => { mockCallWithRequestRepository, [], 'serverless', - featuresPluginMock.createStart() + featuresStart ); const id = savedObject.id; @@ -800,7 +858,7 @@ describe('#update', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const id = savedObject.id; @@ -827,7 +885,7 @@ describe('#update', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const id = savedObject.id; await client.update(id, { ...spaceToUpdate, solution: 'es' }); @@ -857,7 +915,7 @@ describe('#update', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const id = savedObject.id; const actualSpace = await client.update(id, spaceToUpdate); @@ -884,7 +942,7 @@ describe('#update', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const id = savedObject.id; @@ -917,7 +975,7 @@ describe('#update', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const id = savedObject.id; @@ -971,7 +1029,7 @@ describe('#delete', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); await expect(client.delete(id)).rejects.toThrowErrorMatchingInlineSnapshot( @@ -993,7 +1051,7 @@ describe('#delete', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); await client.delete(id); @@ -1016,7 +1074,7 @@ describe('#disableLegacyUrlAliases', () => { mockCallWithRequestRepository, [], 'traditional', - featuresPluginMock.createStart() + featuresStart ); const aliases = [ { targetSpace: 'space1', targetType: 'foo', sourceId: '123' }, 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 4043da9f87225..5d7ae1159f5ea 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.ts @@ -20,6 +20,7 @@ import type { FeaturesPluginStart } from '@kbn/features-plugin/server'; import { isReservedSpace } from '../../common'; import type { spaceV1 as v1 } from '../../common'; import type { ConfigType } from '../config'; +import { withSpaceSolutionDisabledFeatures } from '../lib/utils/space_solution_disabled_features'; const SUPPORTED_GET_SPACE_PURPOSES: v1.GetAllSpacesPurpose[] = [ 'any', @@ -253,7 +254,11 @@ export class SpacesClient implements ISpacesClient { color: savedObject.attributes.color, initials: savedObject.attributes.initials, imageUrl: savedObject.attributes.imageUrl, - disabledFeatures: savedObject.attributes.disabledFeatures ?? [], + disabledFeatures: withSpaceSolutionDisabledFeatures( + this.features.getKibanaFeatures(), + savedObject.attributes.disabledFeatures ?? [], + !this.isServerless ? savedObject.attributes.solution : undefined + ), _reserved: savedObject.attributes._reserved, ...(!this.isServerless ? { solution: savedObject.attributes.solution } : {}), } as v1.Space; diff --git a/x-pack/plugins/stack_connectors/server/connector_types/gemini/gemini.ts b/x-pack/plugins/stack_connectors/server/connector_types/gemini/gemini.ts index 895dfe66d6de4..10aedb52bc6b0 100644 --- a/x-pack/plugins/stack_connectors/server/connector_types/gemini/gemini.ts +++ b/x-pack/plugins/stack_connectors/server/connector_types/gemini/gemini.ts @@ -395,9 +395,7 @@ const formatGeminiPayload = ({ temperature, maxOutputTokens: DEFAULT_TOKEN_LIMIT, }, - ...(systemInstruction - ? { system_instruction: { role: 'user', parts: [{ text: systemInstruction }] } } - : {}), + ...(systemInstruction ? { system_instruction: { parts: [{ text: systemInstruction }] } } : {}), ...(toolConfig ? { tool_config: { diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 28b028fdc3481..cb98cb08e47f6 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -6923,7 +6923,6 @@ "searchIndexDocuments.documentList.resultLimit": "Seuls les {number} premiers résultats sont disponibles pour la pagination. Veuillez utiliser la barre de recherche pour filtrer vos résultats.", "searchIndexDocuments.documentList.resultLimitTitle": "Les résultats sont limités à {number} documents", "searchIndexDocuments.documents.searchField.placeholder": "Rechercher des documents dans cet index", - "searchIndexDocuments.documents.title": "Parcourir des documents", "searchIndexDocuments.result.expandTooltip.allVisible": "Tous les champs sont visibles", "searchIndexDocuments.result.expandTooltip.showFewer": "Afficher {amount} champs en moins", "searchIndexDocuments.result.expandTooltip.showMore": "Afficher {amount} champs en plus", @@ -10775,8 +10774,6 @@ "xpack.apm.onboarding.shared_clients.configure.commands.serviceEnvironmentHint": "Le nom de l'environnement dans lequel ce service est déployé, par exemple \"production\" ou \"test\". Les environnements vous permettent de facilement filtrer les données à un niveau global dans l'interface utilisateur APM. Il est important de garantir la cohérence des noms d'environnements entre les différents agents.", "xpack.apm.onboarding.shared_clients.configure.commands.serviceNameHint": "Le nom de service est le filtre principal dans l'interface utilisateur APM et est utilisé pour regrouper les erreurs et suivre les données ensemble. Caractères autorisés : a-z, A-Z, 0-9, -, _ et espace.", "xpack.apm.onboarding.specProvider.longDescription": "Le monitoring des performances applicatives (APM) collecte les indicateurs et les erreurs de performance approfondies depuis votre application. Cela vous permet de monitorer les performances de milliers d'applications en temps réel. {learnMoreLink}.", - "xpack.apm.pages.alertDetails.alertSummary.actualValue": "Valeur réelle", - "xpack.apm.pages.alertDetails.alertSummary.expectedValue": "Valeur attendue", "xpack.apm.percentOfParent": "({value} de {parentType, select, transaction { transaction } trace {trace} other {parentType inconnu} })", "xpack.apm.profiling.callout.description": "Universal Profiling fournit une visibilité sans précédent du code au milieu du comportement en cours d'exécution de toutes les applications. La fonctionnalité profile chaque ligne de code chez le ou les hôtes qui exécutent vos services, y compris votre code applicatif, le kernel et même les bibliothèque tierces.", "xpack.apm.profiling.callout.dismiss": "Rejeter", @@ -17032,10 +17029,6 @@ "xpack.enterpriseSearch.content.supportedLanguages.spanishLabel": "Espagnol", "xpack.enterpriseSearch.content.supportedLanguages.thaiLabel": "Thaï", "xpack.enterpriseSearch.content.supportedLanguages.universalLabel": "Universel", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.accessControl.label": "Synchronisations de contrôle d'accès", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.content.label": "Synchronisations de contenu", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.legend": "Sélectionnez le type de tâche de synchronisation à afficher.", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.name": "Type de tâche de synchronisation", "xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "Voulez-vous vraiment supprimer le domaine \"{domainUrl}\" et tous ses paramètres ?", "xpack.enterpriseSearch.crawler.addDomainFlyout.description": "Vous pouvez ajouter plusieurs domaines au robot d'indexation de cet index. Ajoutez un autre domaine ici et modifiez les points d'entrée et les règles d'indexation à partir de la page \"Gérer\".", "xpack.enterpriseSearch.crawler.addDomainFlyout.openButtonLabel": "Ajouter un domaine", @@ -17310,7 +17303,6 @@ "xpack.enterpriseSearch.index.connector.syncRules.invalidTitle": "Les ébauches de règles de synchronisation ne sont pas valides", "xpack.enterpriseSearch.index.connector.syncRules.successCallout.applyDraftRulesTitle": "Activer les ébauches de règles", "xpack.enterpriseSearch.index.connector.syncRules.syncRulesLabel": "En savoir plus sur les règles de synchronisation", - "xpack.enterpriseSearch.index.connector.syncRules.title": "Règles de synchronisation ", "xpack.enterpriseSearch.index.connector.syncRules.unsavedChanges": "Vos modifications n'ont pas été enregistrées. Voulez-vous vraiment quitter ?", "xpack.enterpriseSearch.index.connector.syncRules.validatedDescription": "Activez les ébauches de règles pour qu'elles prennent effet à la prochaine synchronisation.", "xpack.enterpriseSearch.index.connector.syncRules.validateDraftTitle": "Enregistrer et valider l'ébauche", @@ -32069,9 +32061,6 @@ "xpack.observability.alertDetailContextualInsights.InsightButtonLabel": "Aidez moi à comprendre cette alerte", "xpack.observability.alertDetails.actionsButtonLabel": "Actions", "xpack.observability.alertDetails.addToCase": "Ajouter au cas", - "xpack.observability.alertDetails.alertSummaryField.rule": "Règle", - "xpack.observability.alertDetails.alertSummaryField.source": "Source", - "xpack.observability.alertDetails.alertSummaryField.tags": "Balises", "xpack.observability.alertDetails.editRule": "Modifier la règle", "xpack.observability.alertDetails.editSnoozeRule": "Répéter la règle", "xpack.observability.alertDetails.errorPromptBody": "Une erreur s'est produite lors du chargement des détails de l'alerte.", @@ -32385,15 +32374,10 @@ "xpack.observability.obltNav.apm.syntheticsGroupTitle": "Synthetics", "xpack.observability.obltNav.applications": "Applications", "xpack.observability.obltNav.devTools": "Outils de développeur", - "xpack.observability.obltNav.getStarted": "Démarrer", "xpack.observability.obltNav.headerSolutionSwitcher.obltSolutionTitle": "Observabilité", "xpack.observability.obltNav.infrastructure": "Infrastructure", "xpack.observability.obltNav.infrastructure.universalProfiling": "Universal Profiling", "xpack.observability.obltNav.management": "Gestion", - "xpack.observability.obltNav.ml.aiAndMlGroupTitle": "IA et ML", - "xpack.observability.obltNav.ml.changePointDetection": "Modifier la détection du point", - "xpack.observability.obltNav.ml.job.notifications": "Notifications de tâches", - "xpack.observability.obltNav.ml.logRateAnalysis": "Analyse du taux de log", "xpack.observability.obltNav.otherTools": "Autres outils", "xpack.observability.obltNav.otherTools.logsStream": "Flux de logs", "xpack.observability.obltNav.stackManagement": "Gestion de la Suite", @@ -41240,7 +41224,6 @@ "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInHours": "{duration} dernières heures", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInMinutes": "{duration} dernières minutes", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInSeconds": "{duration} dernières secondes", - "xpack.slo.burnRateRule.alertDetailsAppSection.summaryField.slo": "SLO", "xpack.slo.burnRateRuleEditor.h5.chooseASLOToMonitorLabel": "Choisir un SLO pour monitorer", "xpack.slo.burnRateRuleEditor.h5.defineMultipleBurnRateLabel": "Définir des fenêtres du taux d'avancement multiples", "xpack.slo.burnRates.fromRange.label": "{duration}h", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 7df47bf1efe61..983ed0097e5f0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6677,7 +6677,6 @@ "searchIndexDocuments.documentList.resultLimit": "最初の{number}件の結果のみがページ制御できます。結果を絞り込むには、検索バーを使用してください。", "searchIndexDocuments.documentList.resultLimitTitle": "結果は{number}ドキュメントに制限されています。", "searchIndexDocuments.documents.searchField.placeholder": "このインデックスでドキュメントを検索", - "searchIndexDocuments.documents.title": "ドキュメントを参照", "searchIndexDocuments.result.expandTooltip.allVisible": "すべてのフィールドが表示されます", "searchIndexDocuments.result.expandTooltip.showFewer": "表示するフィールド数を{amount}個減らす", "searchIndexDocuments.result.expandTooltip.showMore": "表示するフィールド数を{amount}個増やす", @@ -10524,8 +10523,6 @@ "xpack.apm.onboarding.shared_clients.configure.commands.serviceEnvironmentHint": "このサービスがデプロイされている環境の名前(例:「本番」、「ステージング」)。環境では、APM UIでグローバルレベルで簡単にデータをフィルタリングできます。すべてのエージェントで環境の命名方法を統一することが重要です。", "xpack.apm.onboarding.shared_clients.configure.commands.serviceNameHint": "このサービス名はAPM UIの主フィルターであり、エラーとトレースデータをグループ化するために使用されます。使用できる文字はA-Z、0-9、-、_、スペースです。", "xpack.apm.onboarding.specProvider.longDescription": "アプリケーションパフォーマンスモニタリング(APM)は、アプリケーション内から詳細なパフォーマンスメトリックやエラーを収集します。何千ものアプリケーションのパフォーマンスをリアルタイムで監視できます。{learnMoreLink}。", - "xpack.apm.pages.alertDetails.alertSummary.actualValue": "実際の値", - "xpack.apm.pages.alertDetails.alertSummary.expectedValue": "想定された値", "xpack.apm.percentOfParent": "({value} of {parentType, select, transaction { トランザクション } trace {トレース} other {不明なparentType} })", "xpack.apm.profiling.callout.description": "ユニバーサルプロファイリングは、すべてのアプリケーションの実行時の動作に関して、かつてないほどコードを可視化します。アプリケーションコードだけでなく、カーネルやサードパーティライブラリも含め、サービスを実行するホスト上のすべてのコード行をプロファイリングします。", "xpack.apm.profiling.callout.dismiss": "閉じる", @@ -16778,10 +16775,6 @@ "xpack.enterpriseSearch.content.supportedLanguages.spanishLabel": "スペイン語", "xpack.enterpriseSearch.content.supportedLanguages.thaiLabel": "タイ語", "xpack.enterpriseSearch.content.supportedLanguages.universalLabel": "ユニバーサル", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.accessControl.label": "アクセス制御同期", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.content.label": "コンテンツ同期", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.legend": "表示する同期ジョブタイプを選択します。", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.name": "同期ジョブタイプ", "xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "ドメイン\"{domainUrl}\"とすべての設定を削除しますか?", "xpack.enterpriseSearch.crawler.addDomainFlyout.description": "複数のドメインをこのインデックスのWebクローラーに追加できます。ここで別のドメインを追加して、[管理]ページからエントリポイントとクロールルールを変更します。", "xpack.enterpriseSearch.crawler.addDomainFlyout.openButtonLabel": "ドメインを追加", @@ -17056,7 +17049,6 @@ "xpack.enterpriseSearch.index.connector.syncRules.invalidTitle": "ドラフト同期ルールが無効です", "xpack.enterpriseSearch.index.connector.syncRules.successCallout.applyDraftRulesTitle": "ドラフトルールをアクティブ化", "xpack.enterpriseSearch.index.connector.syncRules.syncRulesLabel": "同期ルールの詳細", - "xpack.enterpriseSearch.index.connector.syncRules.title": "同期ルール ", "xpack.enterpriseSearch.index.connector.syncRules.unsavedChanges": "変更は保存されていません。終了してよろしいですか?", "xpack.enterpriseSearch.index.connector.syncRules.validatedDescription": "ドラフトルールをアクティブ化し、次回の同期で有効にします。", "xpack.enterpriseSearch.index.connector.syncRules.validateDraftTitle": "ドラフトを保存して検証", @@ -31813,9 +31805,6 @@ "xpack.observability.alertDetailContextualInsights.InsightButtonLabel": "このアラートを理解できるように支援してください", "xpack.observability.alertDetails.actionsButtonLabel": "アクション", "xpack.observability.alertDetails.addToCase": "ケースに追加", - "xpack.observability.alertDetails.alertSummaryField.rule": "ルール", - "xpack.observability.alertDetails.alertSummaryField.source": "送信元", - "xpack.observability.alertDetails.alertSummaryField.tags": "タグ", "xpack.observability.alertDetails.editRule": "ルールを編集", "xpack.observability.alertDetails.editSnoozeRule": "ルールをスヌーズ", "xpack.observability.alertDetails.errorPromptBody": "アラート詳細の読み込みエラーが発生しました。", @@ -32130,15 +32119,10 @@ "xpack.observability.obltNav.apm.syntheticsGroupTitle": "Synthetics", "xpack.observability.obltNav.applications": "アプリケーション", "xpack.observability.obltNav.devTools": "開発者ツール", - "xpack.observability.obltNav.getStarted": "使ってみる", "xpack.observability.obltNav.headerSolutionSwitcher.obltSolutionTitle": "Observability", "xpack.observability.obltNav.infrastructure": "インフラストラクチャー", "xpack.observability.obltNav.infrastructure.universalProfiling": "ユニバーサルプロファイリング", "xpack.observability.obltNav.management": "管理", - "xpack.observability.obltNav.ml.aiAndMlGroupTitle": "AI & ML", - "xpack.observability.obltNav.ml.changePointDetection": "変化点検出", - "xpack.observability.obltNav.ml.job.notifications": "ジョブ通知", - "xpack.observability.obltNav.ml.logRateAnalysis": "ログレート分析", "xpack.observability.obltNav.otherTools": "その他のツール", "xpack.observability.obltNav.otherTools.logsStream": "ログストリーム", "xpack.observability.obltNav.stackManagement": "スタック管理", @@ -40984,7 +40968,6 @@ "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInHours": "過去{duration}時間", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInMinutes": "過去{duration}分", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInSeconds": "過去{duration}秒", - "xpack.slo.burnRateRule.alertDetailsAppSection.summaryField.slo": "SLO", "xpack.slo.burnRateRule.name": "{name}バーンレートルール", "xpack.slo.burnRateRuleEditor.h5.chooseASLOToMonitorLabel": "監視するSLOを選択", "xpack.slo.burnRateRuleEditor.h5.defineMultipleBurnRateLabel": "複数のバーンレート時間枠を定義", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 581eb4ace7963..092a3924977c7 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6691,7 +6691,6 @@ "searchIndexDocuments.documentList.resultLimit": "仅前 {number} 个结果可用于分页。请使用搜索栏筛选结果。", "searchIndexDocuments.documentList.resultLimitTitle": "结果仅限于 {number} 个文档", "searchIndexDocuments.documents.searchField.placeholder": "在此索引中搜索文档", - "searchIndexDocuments.documents.title": "浏览文档", "searchIndexDocuments.result.expandTooltip.allVisible": "所有字段均可见", "searchIndexDocuments.result.expandTooltip.showFewer": "显示少于 {amount} 个字段", "searchIndexDocuments.result.expandTooltip.showMore": "显示多于 {amount} 个字段", @@ -10546,8 +10545,6 @@ "xpack.apm.onboarding.shared_clients.configure.commands.serviceEnvironmentHint": "在其中部署此服务的环境的名称,如“生产”或“暂存”。在 APM UI 中,您可以通过环境在全局级别轻松筛选数据。跨代理命名环境时,保持一致至关重要。", "xpack.apm.onboarding.shared_clients.configure.commands.serviceNameHint": "服务名称是 APM UI 中的初级筛选,用于分组错误并跟踪数据。允许使用的字符包括 a-z、A-Z、0-9、-、_ 和空格。", "xpack.apm.onboarding.specProvider.longDescription": "应用程序性能监测 (APM) 从您的应用程序内收集深入全面的性能指标和错误。其允许您实时监测数以千计的应用程序的性能。{learnMoreLink}。", - "xpack.apm.pages.alertDetails.alertSummary.actualValue": "实际值", - "xpack.apm.pages.alertDetails.alertSummary.expectedValue": "预期值", "xpack.apm.percentOfParent": "({parentType, select, transaction {事务} trace {追溯} other {parentType 未知}}的 {value})", "xpack.apm.profiling.callout.description": "Universal Profiling 为所有应用程序的运行时行为提供了前所未有的代码可见性。它会剖析运行服务的主机上的每一行代码,不仅包括您的应用程序代码,而且包括内核和第三方库。", "xpack.apm.profiling.callout.dismiss": "关闭", @@ -16807,10 +16804,6 @@ "xpack.enterpriseSearch.content.supportedLanguages.spanishLabel": "西班牙语", "xpack.enterpriseSearch.content.supportedLanguages.thaiLabel": "泰语", "xpack.enterpriseSearch.content.supportedLanguages.universalLabel": "通用", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.accessControl.label": "访问控制同步", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.content.label": "内容同步", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.legend": "选择要显示的同步作业类型。", - "xpack.enterpriseSearch.content.syncJobs.lastSync.tableSelector.name": "同步作业类型", "xpack.enterpriseSearch.crawler.action.deleteDomain.confirmationPopupMessage": "确定要移除域“{domainUrl}”和其所有设置?", "xpack.enterpriseSearch.crawler.addDomainFlyout.description": "可以将多个域添加到此索引的网络爬虫。在此添加其他域并从“管理”页面修改入口点和爬网规则。", "xpack.enterpriseSearch.crawler.addDomainFlyout.openButtonLabel": "添加域", @@ -17085,7 +17078,6 @@ "xpack.enterpriseSearch.index.connector.syncRules.invalidTitle": "同步规则草案无效", "xpack.enterpriseSearch.index.connector.syncRules.successCallout.applyDraftRulesTitle": "激活规则草案", "xpack.enterpriseSearch.index.connector.syncRules.syncRulesLabel": "详细了解同步规则", - "xpack.enterpriseSearch.index.connector.syncRules.title": "同步规则 ", "xpack.enterpriseSearch.index.connector.syncRules.unsavedChanges": "您的更改尚未更改。是否确定要离开?", "xpack.enterpriseSearch.index.connector.syncRules.validatedDescription": "激活规则草案以在下次同步时生效。", "xpack.enterpriseSearch.index.connector.syncRules.validateDraftTitle": "保存并验证草案", @@ -31856,9 +31848,6 @@ "xpack.observability.alertDetailContextualInsights.InsightButtonLabel": "帮助我了解此告警", "xpack.observability.alertDetails.actionsButtonLabel": "操作", "xpack.observability.alertDetails.addToCase": "添加到案例", - "xpack.observability.alertDetails.alertSummaryField.rule": "规则", - "xpack.observability.alertDetails.alertSummaryField.source": "源", - "xpack.observability.alertDetails.alertSummaryField.tags": "标签", "xpack.observability.alertDetails.editRule": "编辑规则", "xpack.observability.alertDetails.editSnoozeRule": "暂停规则", "xpack.observability.alertDetails.errorPromptBody": "加载告警详情时出错。", @@ -32173,15 +32162,10 @@ "xpack.observability.obltNav.apm.syntheticsGroupTitle": "Synthetics", "xpack.observability.obltNav.applications": "应用程序", "xpack.observability.obltNav.devTools": "开发者工具", - "xpack.observability.obltNav.getStarted": "开始使用", "xpack.observability.obltNav.headerSolutionSwitcher.obltSolutionTitle": "Observability", "xpack.observability.obltNav.infrastructure": "基础设施", "xpack.observability.obltNav.infrastructure.universalProfiling": "Universal Profiling", "xpack.observability.obltNav.management": "管理", - "xpack.observability.obltNav.ml.aiAndMlGroupTitle": "AI 和 ML", - "xpack.observability.obltNav.ml.changePointDetection": "更改点检测", - "xpack.observability.obltNav.ml.job.notifications": "作业通知", - "xpack.observability.obltNav.ml.logRateAnalysis": "日志速率分析", "xpack.observability.obltNav.otherTools": "其他工具", "xpack.observability.obltNav.otherTools.logsStream": "日志流", "xpack.observability.obltNav.stackManagement": "Stack Management", @@ -41030,7 +41014,6 @@ "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInHours": "过去 {duration} 小时", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInMinutes": "过去 {duration} 分钟", "xpack.slo.burnRateRule.alertDetailsAppSection.lastDurationInSeconds": "过去 {duration} 秒", - "xpack.slo.burnRateRule.alertDetailsAppSection.summaryField.slo": "SLO", "xpack.slo.burnRateRule.name": "{name} 消耗速度规则", "xpack.slo.burnRateRuleEditor.h5.chooseASLOToMonitorLabel": "选择要监测的 SLO", "xpack.slo.burnRateRuleEditor.h5.defineMultipleBurnRateLabel": "定义多个消耗速度窗口", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/url_synced_alerts_search_bar.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/url_synced_alerts_search_bar.tsx index 71594e3206ffd..de89eb9715733 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/url_synced_alerts_search_bar.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_search_bar/url_synced_alerts_search_bar.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { BoolQuery } from '@kbn/es-query'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { i18n } from '@kbn/i18n'; @@ -61,17 +61,23 @@ export const UrlSyncedAlertsSearchBar = ({ const [spaceId, setSpaceId] = useState(); const { + // KQL bar query kuery, onKueryChange, + // KQL bar filters filters, onFiltersChange, + // Controls bar filters controlFilters, onControlFiltersChange, + // Time range rangeFrom, onRangeFromChange, rangeTo, onRangeToChange, + // Controls bar configuration filterControls, + // Saved KQL query savedQuery, setSavedQuery, clearSavedQuery, @@ -131,6 +137,11 @@ export const UrlSyncedAlertsSearchBar = ({ [onKueryChange, onRangeFromChange, onRangeToChange, setSavedQuery, timeFilterService] ); + const filterControlsStorageKey = useMemo( + () => ['alertsSearchBar', spaceId, 'filterControls'].filter(Boolean).join('.'), + [spaceId] + ); + return ( <> { const { command, payload } = action; const { watch } = state; + const watchTypes = Watch.getWatchTypes(); + const WatchType = + watch && has(watchTypes, watch.type) && isFunction(watchTypes[watch.type]) + ? watchTypes[watch.type] + : null; + switch (command) { case 'setWatch': return { @@ -57,23 +63,33 @@ const watchReducer = (state: any, action: any) => { if (isEqual(watch[property], value)) { return state; } else { - return { - ...state, - watch: new (Watch.getWatchTypes()[watch.type])({ - ...watch, - [property]: value, - }), - }; + if (WatchType) { + return { + ...state, + watch: new WatchType({ + ...watch, + [property]: value, + }), + }; + } + + return state; } case 'addAction': const { type, defaults } = payload; - const newWatch = new (Watch.getWatchTypes()[watch.type])(watch); - newWatch.createAction(type, defaults); - return { - ...state, - watch: newWatch, - }; + + if (WatchType) { + const newWatch = new WatchType(watch); + newWatch.createAction(type, defaults); + + return { + ...state, + watch: newWatch, + }; + } else { + return state; + } case 'setError': return { @@ -117,9 +133,13 @@ export const WatchEditPage = ({ dispatch({ command: 'setError', payload: error.body }); } } else if (type) { - const WatchType = Watch.getWatchTypes()[type]; - if (WatchType) { + const watchTypes = Watch.getWatchTypes(); + + if (has(watchTypes, type) && isFunction(watchTypes[type])) { + const WatchType = watchTypes[type]; dispatch({ command: 'setWatch', payload: new WatchType() }); + } else { + dispatch({ command: 'setError', payload: { message: 'Invalid watch type' } }); } } }; diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts new file mode 100644 index 0000000000000..056bde27fc33c --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/degraded_field_analyze.ts @@ -0,0 +1,157 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { log, timerange } from '@kbn/apm-synthtrace-client'; +import { SupertestWithRoleScopeType } from '../../../services'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; +import { createBackingIndexNameWithoutVersion, setDataStreamSettings } from './es_utils'; + +const MORE_THAN_1024_CHARS = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const roleScopedSupertest = getService('roleScopedSupertest'); + const synthtrace = getService('logsSynthtraceEsClient'); + const esClient = getService('es'); + const start = '2024-09-20T11:00:00.000Z'; + const end = '2024-09-20T11:01:00.000Z'; + const type = 'logs'; + const dataset = 'synth.good'; + const namespace = 'default'; + const serviceName = 'my-service'; + const hostName = 'synth-host'; + const dataStreamName = `${type}-${dataset}-${namespace}`; + + async function callApiAs({ + roleScopedSupertestWithCookieCredentials, + apiParams: { dataStream, degradedField, lastBackingIndex }, + }: { + roleScopedSupertestWithCookieCredentials: SupertestWithRoleScopeType; + apiParams: { + dataStream: string; + degradedField: string; + lastBackingIndex: string; + }; + }) { + return roleScopedSupertestWithCookieCredentials + .get( + `/internal/dataset_quality/data_streams/${dataStream}/degraded_field/${degradedField}/analyze` + ) + .query({ lastBackingIndex }); + } + + describe('Degraded field analyze', () => { + let supertestAdminWithCookieCredentials: SupertestWithRoleScopeType; + + before(async () => { + supertestAdminWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( + 'admin', + { + useCookieHeader: true, + withInternalHeaders: true, + } + ); + }); + + describe('gets limit analysis for a given datastream and degraded field', () => { + before(async () => { + await synthtrace.index([ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is a log message') + .timestamp(timestamp) + .dataset(dataset) + .namespace(namespace) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': serviceName, + 'host.name': hostName, + test_field: [MORE_THAN_1024_CHARS, 'hello world'], + }) + ), + ]); + }); + + it('should return default limits and should return isFieldLimitIssue as false', async () => { + const resp = await callApiAs({ + roleScopedSupertestWithCookieCredentials: supertestAdminWithCookieCredentials, + apiParams: { + dataStream: dataStreamName, + degradedField: 'test_field', + lastBackingIndex: `${createBackingIndexNameWithoutVersion({ + type, + dataset, + namespace, + })}-000001`, + }, + }); + + expect(resp.body.isFieldLimitIssue).to.be(false); + expect(resp.body.fieldCount).to.be(25); + expect(resp.body.fieldMapping).to.eql({ type: 'keyword', ignore_above: 1024 }); + expect(resp.body.totalFieldLimit).to.be(1000); + expect(resp.body.ignoreMalformed).to.be(true); + expect(resp.body.nestedFieldLimit).to.be(50); + }); + + it('should return updated limits and should return isFieldLimitIssue as true', async () => { + await setDataStreamSettings(esClient, dataStreamName, { + 'mapping.total_fields.limit': 25, + }); + await synthtrace.index([ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is a log message') + .timestamp(timestamp) + .dataset(dataset) + .namespace(namespace) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': serviceName, + 'host.name': hostName, + test_field: [MORE_THAN_1024_CHARS, 'hello world'], + 'cloud.region': 'us-east-1', + }) + ), + ]); + + const resp = await callApiAs({ + roleScopedSupertestWithCookieCredentials: supertestAdminWithCookieCredentials, + apiParams: { + dataStream: dataStreamName, + degradedField: 'cloud.region', + lastBackingIndex: `${createBackingIndexNameWithoutVersion({ + type, + dataset, + namespace, + })}-000001`, + }, + }); + + expect(resp.body.isFieldLimitIssue).to.be(true); + expect(resp.body.fieldCount).to.be(25); + expect(resp.body.fieldMapping).to.be(undefined); // As the field limit was reached, field cannot be mapped + expect(resp.body.totalFieldLimit).to.be(25); + expect(resp.body.ignoreMalformed).to.be(true); + expect(resp.body.nestedFieldLimit).to.be(50); + }); + + after(async () => { + await synthtrace.clean(); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/es_utils.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/es_utils.ts new file mode 100644 index 0000000000000..0e041781122cd --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/es_utils.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Client } from '@elastic/elasticsearch'; +import { IndicesIndexSettings } from '@elastic/elasticsearch/lib/api/types'; + +function getCurrentDateFormatted() { + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}.${month}.${day}`; +} + +export function createBackingIndexNameWithoutVersion({ + type, + dataset, + namespace = 'default', +}: { + type: string; + dataset: string; + namespace: string; +}) { + return `.ds-${type}-${dataset}-${namespace}-${getCurrentDateFormatted()}`; +} + +export async function setDataStreamSettings( + esClient: Client, + name: string, + settings: IndicesIndexSettings +) { + return esClient.indices.putSettings({ + index: name, + settings, + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/index.ts index 73c58952b490b..0c660dda0a445 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/index.ts @@ -9,6 +9,7 @@ import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_cont export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { describe('Dataset quality', () => { - loadTestFile(require.resolve('./integrations/integrations')); + loadTestFile(require.resolve('./integrations')); + loadTestFile(require.resolve('./degraded_field_analyze')); }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integrations/integrations.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integrations.ts similarity index 96% rename from x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integrations/integrations.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integrations.ts index 0e6d319cc0ff6..910dd84bb309e 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integrations/integrations.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/dataset_quality/integrations.ts @@ -8,8 +8,8 @@ import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services'; import expect from '@kbn/expect'; import { APIReturnType } from '@kbn/dataset-quality-plugin/common/rest'; -import { CustomIntegration } from '../../../../services/package_api'; -import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { CustomIntegration } from '../../../services/package_api'; +import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const samlAuth = getService('samlAuth'); diff --git a/x-pack/test/api_integration/deployment_agnostic/services/index.ts b/x-pack/test/api_integration/deployment_agnostic/services/index.ts index bdaa65c139035..bea63ea216c93 100644 --- a/x-pack/test/api_integration/deployment_agnostic/services/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/services/index.ts @@ -12,6 +12,7 @@ import { deploymentAgnosticServices } from './deployment_agnostic_services'; import { PackageApiProvider } from './package_api'; import { RoleScopedSupertestProvider, SupertestWithRoleScope } from './role_scoped_supertest'; import { SloApiProvider } from './slo_api'; +import { LogsSynthtraceEsClientProvider } from './logs_synthtrace_es_client'; export type { InternalRequestHeader, @@ -28,6 +29,7 @@ export const services = { packageApi: PackageApiProvider, sloApi: SloApiProvider, roleScopedSupertest: RoleScopedSupertestProvider, + logsSynthtraceEsClient: LogsSynthtraceEsClientProvider, // create a new deployment-agnostic service and load here }; diff --git a/x-pack/test/api_integration/deployment_agnostic/services/logs_synthtrace_es_client.ts b/x-pack/test/api_integration/deployment_agnostic/services/logs_synthtrace_es_client.ts new file mode 100644 index 0000000000000..2ad02231c4490 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/services/logs_synthtrace_es_client.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createLogger, LogLevel, LogsSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context'; + +export function LogsSynthtraceEsClientProvider({ + getService, +}: DeploymentAgnosticFtrProviderContext) { + return new LogsSynthtraceEsClient({ + client: getService('es'), + logger: createLogger(LogLevel.info), + refreshAfterIndex: true, + }); +} diff --git a/x-pack/test/dataset_quality_api_integration/tests/data_streams/data_stream_settings.spec.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/data_stream_settings.spec.ts index eafc966dc4bda..45f37b44983aa 100644 --- a/x-pack/test/dataset_quality_api_integration/tests/data_streams/data_stream_settings.spec.ts +++ b/x-pack/test/dataset_quality_api_integration/tests/data_streams/data_stream_settings.spec.ts @@ -15,6 +15,7 @@ import { getDataStreamSettingsOfEarliestIndex, rolloverDataStream, } from '../../utils'; +import { createBackingIndexNameWithoutVersion } from './es_utils'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); @@ -110,19 +111,30 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(resp.body).eql(defaultDataStreamPrivileges); }); - it('returns "createdOn" correctly', async () => { + it('returns "createdOn", "integration" and "lastBackingIndexName" correctly when available', async () => { const dataStreamSettings = await getDataStreamSettingsOfEarliestIndex( esClient, - `${type}-${dataset}-${namespace}` + `${type}-${integrationDataset}-${namespace}` ); const resp = await callApiAs( 'datasetQualityMonitorUser', - `${type}-${dataset}-${namespace}` + `${type}-${integrationDataset}-${namespace}` ); expect(resp.body.createdOn).to.be(Number(dataStreamSettings?.index?.creation_date)); + expect(resp.body.integration).to.be('apache'); + expect(resp.body.lastBackingIndexName).to.be( + `${createBackingIndexNameWithoutVersion({ + type, + dataset: integrationDataset, + namespace, + })}-000001` + ); + expect(resp.body.datasetUserPrivileges).to.eql( + defaultDataStreamPrivileges.datasetUserPrivileges + ); }); - it('returns "createdOn" correctly for rolled over dataStream', async () => { + it('returns "createdOn" and "lastBackingIndexName" for rolled over dataStream', async () => { await rolloverDataStream(esClient, `${type}-${dataset}-${namespace}`); const dataStreamSettings = await getDataStreamSettingsOfEarliestIndex( esClient, @@ -133,21 +145,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { `${type}-${dataset}-${namespace}` ); expect(resp.body.createdOn).to.be(Number(dataStreamSettings?.index?.creation_date)); - }); - - it('returns "createdOn" and "integration" correctly when available', async () => { - const dataStreamSettings = await getDataStreamSettingsOfEarliestIndex( - esClient, - `${type}-${integrationDataset}-${namespace}` - ); - const resp = await callApiAs( - 'datasetQualityMonitorUser', - `${type}-${integrationDataset}-${namespace}` - ); - expect(resp.body.createdOn).to.be(Number(dataStreamSettings?.index?.creation_date)); - expect(resp.body.integration).to.be('apache'); - expect(resp.body.datasetUserPrivileges).to.eql( - defaultDataStreamPrivileges.datasetUserPrivileges + expect(resp.body.lastBackingIndexName).to.be( + `${createBackingIndexNameWithoutVersion({ type, dataset, namespace })}-000002` ); }); diff --git a/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_fields.spec.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_fields.spec.ts index fe6022067776f..100783e26e0ee 100644 --- a/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_fields.spec.ts +++ b/x-pack/test/dataset_quality_api_integration/tests/data_streams/degraded_fields.spec.ts @@ -10,6 +10,8 @@ import expect from '@kbn/expect'; import { DegradedField } from '@kbn/dataset-quality-plugin/common/api_types'; import { DatasetQualityApiClientKey } from '../../common/config'; import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { rolloverDataStream } from '../../utils'; +import { createBackingIndexNameWithoutVersion } from './es_utils'; const MORE_THAN_1024_CHARS = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?'; @@ -18,6 +20,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const synthtrace = getService('logSynthtraceEsClient'); const datasetQualityApiClient = getService('datasetQualityApiClient'); + const esClient = getService('es'); const start = '2024-05-22T08:00:00.000Z'; const end = '2024-05-23T08:02:00.000Z'; const type = 'logs'; @@ -130,6 +133,56 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(logLevelTimeSeries).to.eql(logsTimeSeriesData); }); + + it('should return the backing index where the ignored field was last seen', async () => { + await rolloverDataStream(esClient, `${type}-${degradedFieldDataset}-${namespace}`); + await synthtrace.index([ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is a error message') + .logLevel(MORE_THAN_1024_CHARS) + .timestamp(timestamp) + .dataset(degradedFieldDataset) + .namespace(namespace) + .defaults({ + 'log.file.path': '/error.log', + 'service.name': serviceName + 1, + }) + ), + ]); + + const resp = await callApiAs( + 'datasetQualityMonitorUser', + `${type}-${degradedFieldDataset}-${namespace}` + ); + + const logLevelLastBackingIndex = resp.body.degradedFields.find( + (dFields) => dFields.name === 'log.level' + )?.indexFieldWasLastPresentIn; + + const traceIdLastBackingIndex = resp.body.degradedFields.find( + (dFields) => dFields.name === 'trace.id' + )?.indexFieldWasLastPresentIn; + + expect(logLevelLastBackingIndex).to.be( + `${createBackingIndexNameWithoutVersion({ + type, + dataset: degradedFieldDataset, + namespace, + })}-000002` + ); + expect(traceIdLastBackingIndex).to.be( + `${createBackingIndexNameWithoutVersion({ + type, + dataset: degradedFieldDataset, + namespace, + })}-000001` + ); + }); }); }); } diff --git a/x-pack/test/dataset_quality_api_integration/tests/data_streams/es_utils.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/es_utils.ts index 607522089952f..d4b7eb2dc0824 100644 --- a/x-pack/test/dataset_quality_api_integration/tests/data_streams/es_utils.ts +++ b/x-pack/test/dataset_quality_api_integration/tests/data_streams/es_utils.ts @@ -6,6 +6,7 @@ */ import { Client } from '@elastic/elasticsearch'; +import { IndicesIndexSettings } from '@elastic/elasticsearch/lib/api/types'; export async function addIntegrationToLogIndexTemplate({ esClient, @@ -52,3 +53,35 @@ export async function cleanLogIndexTemplate({ esClient }: { esClient: Client }) }, }); } + +function getCurrentDateFormatted() { + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}.${month}.${day}`; +} + +export function createBackingIndexNameWithoutVersion({ + type, + dataset, + namespace = 'default', +}: { + type: string; + dataset: string; + namespace: string; +}) { + return `.ds-${type}-${dataset}-${namespace}-${getCurrentDateFormatted()}`; +} + +export async function setDataStreamSettings( + esClient: Client, + name: string, + settings: IndicesIndexSettings +) { + return esClient.indices.putSettings({ + index: name, + settings, + }); +} diff --git a/x-pack/test/functional/apps/dataset_quality/dataset_quality_details_degraded_field_flyout.ts b/x-pack/test/functional/apps/dataset_quality/dataset_quality_details_degraded_field_flyout.ts deleted file mode 100644 index 4729f5a14629d..0000000000000 --- a/x-pack/test/functional/apps/dataset_quality/dataset_quality_details_degraded_field_flyout.ts +++ /dev/null @@ -1,108 +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 expect from '@kbn/expect'; -import { DatasetQualityFtrProviderContext } from './config'; -import { - createDegradedFieldsRecord, - datasetNames, - defaultNamespace, - getInitialTestLogs, - ANOTHER_1024_CHARS, - MORE_THAN_1024_CHARS, -} from './data'; - -export default function ({ getService, getPageObjects }: DatasetQualityFtrProviderContext) { - const PageObjects = getPageObjects([ - 'common', - 'navigationalSearch', - 'observabilityLogsExplorer', - 'datasetQuality', - ]); - const testSubjects = getService('testSubjects'); - const synthtrace = getService('logSynthtraceEsClient'); - const retry = getService('retry'); - const to = '2024-01-01T12:00:00.000Z'; - const degradedDatasetName = datasetNames[2]; - const degradedDataStreamName = `logs-${degradedDatasetName}-${defaultNamespace}`; - - describe('Degraded fields flyout', () => { - before(async () => { - await synthtrace.index([ - // Ingest basic logs - getInitialTestLogs({ to, count: 4 }), - // Ingest Degraded Logs - createDegradedFieldsRecord({ - to: new Date().toISOString(), - count: 2, - dataset: degradedDatasetName, - }), - ]); - }); - - after(async () => { - await synthtrace.clean(); - }); - - describe('degraded field flyout open-close', () => { - it('should open and close the flyout when user clicks on the expand button', async () => { - await PageObjects.datasetQuality.navigateToDetails({ - dataStream: degradedDataStreamName, - }); - - await PageObjects.datasetQuality.openDegradedFieldFlyout('test_field'); - - await testSubjects.existOrFail( - PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout - ); - - await PageObjects.datasetQuality.closeFlyout(); - - await testSubjects.missingOrFail( - PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout - ); - }); - - it('should open the flyout when navigating to the page with degradedField in URL State', async () => { - await PageObjects.datasetQuality.navigateToDetails({ - dataStream: degradedDataStreamName, - expandedDegradedField: 'test_field', - }); - - await testSubjects.existOrFail( - PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout - ); - - await PageObjects.datasetQuality.closeFlyout(); - }); - }); - - describe('values exist', () => { - it('should display the degraded field values', async () => { - await PageObjects.datasetQuality.navigateToDetails({ - dataStream: degradedDataStreamName, - expandedDegradedField: 'test_field', - }); - - await retry.tryForTime(5000, async () => { - const cloudAvailabilityZoneValueExists = await PageObjects.datasetQuality.doesTextExist( - 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-values', - ANOTHER_1024_CHARS - ); - const cloudAvailabilityZoneValue2Exists = await PageObjects.datasetQuality.doesTextExist( - 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-values', - MORE_THAN_1024_CHARS - ); - expect(cloudAvailabilityZoneValueExists).to.be(true); - expect(cloudAvailabilityZoneValue2Exists).to.be(true); - }); - - await PageObjects.datasetQuality.closeFlyout(); - }); - }); - }); -} diff --git a/x-pack/test/functional/apps/dataset_quality/degraded_field_flyout.ts b/x-pack/test/functional/apps/dataset_quality/degraded_field_flyout.ts new file mode 100644 index 0000000000000..517a7f2ad93fc --- /dev/null +++ b/x-pack/test/functional/apps/dataset_quality/degraded_field_flyout.ts @@ -0,0 +1,419 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import moment from 'moment/moment'; +import { generateShortId, log, timerange } from '@kbn/apm-synthtrace-client'; +import { DatasetQualityFtrProviderContext } from './config'; +import { + createDegradedFieldsRecord, + datasetNames, + defaultNamespace, + getInitialTestLogs, + ANOTHER_1024_CHARS, + MORE_THAN_1024_CHARS, +} from './data'; + +export default function ({ getService, getPageObjects }: DatasetQualityFtrProviderContext) { + const PageObjects = getPageObjects([ + 'common', + 'navigationalSearch', + 'observabilityLogsExplorer', + 'datasetQuality', + ]); + const testSubjects = getService('testSubjects'); + const synthtrace = getService('logSynthtraceEsClient'); + const retry = getService('retry'); + const to = new Date().toISOString(); + const degradedDatasetName = datasetNames[2]; + const degradedDataStreamName = `logs-${degradedDatasetName}-${defaultNamespace}`; + + const degradedDatasetWithLimitsName = 'degraded.dataset.rca'; + const degradedDatasetWithLimitDataStreamName = `logs-${degradedDatasetWithLimitsName}-${defaultNamespace}`; + const serviceName = 'test_service'; + const count = 5; + + describe('Degraded fields flyout', () => { + before(async () => { + await synthtrace.index([ + // Ingest basic logs + getInitialTestLogs({ to, count: 4 }), + // Ingest Degraded Logs + createDegradedFieldsRecord({ + to: new Date().toISOString(), + count: 2, + dataset: degradedDatasetName, + }), + ]); + }); + + after(async () => { + await synthtrace.clean(); + }); + + describe('degraded field flyout open-close', () => { + it('should open and close the flyout when user clicks on the expand button', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDataStreamName, + }); + + await PageObjects.datasetQuality.openDegradedFieldFlyout('test_field'); + + await testSubjects.existOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + + await PageObjects.datasetQuality.closeFlyout(); + + await testSubjects.missingOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + }); + + it('should open the flyout when navigating to the page with degradedField in URL State', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDataStreamName, + expandedDegradedField: 'test_field', + }); + + await testSubjects.existOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + + await PageObjects.datasetQuality.closeFlyout(); + }); + }); + + describe('values exist', () => { + it('should display the degraded field values', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDataStreamName, + expandedDegradedField: 'test_field', + }); + + await retry.tryForTime(5000, async () => { + const cloudAvailabilityZoneValueExists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-values', + ANOTHER_1024_CHARS + ); + const cloudAvailabilityZoneValue2Exists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-values', + MORE_THAN_1024_CHARS + ); + expect(cloudAvailabilityZoneValueExists).to.be(true); + expect(cloudAvailabilityZoneValue2Exists).to.be(true); + }); + + await PageObjects.datasetQuality.closeFlyout(); + }); + }); + + describe('testing root cause for ignored fields', () => { + before(async () => { + // Ingest Degraded Logs with 25 fields + await synthtrace.index([ + timerange(moment(to).subtract(count, 'minute'), moment(to)) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(1) + .fill(0) + .flatMap(() => + log + .create() + .dataset(degradedDatasetWithLimitsName) + .message('a log message') + .logLevel(MORE_THAN_1024_CHARS) + .service(serviceName) + .namespace(defaultNamespace) + .defaults({ + 'service.name': serviceName, + 'trace.id': generateShortId(), + test_field: [MORE_THAN_1024_CHARS, 'hello world'], + }) + .timestamp(timestamp) + ); + }), + ]); + + // Set Limit of 25 + await PageObjects.datasetQuality.setDataStreamSettings( + degradedDatasetWithLimitDataStreamName, + { + 'mapping.total_fields.limit': 25, + } + ); + + // Ingest Degraded Logs with 26 field + await synthtrace.index([ + timerange(moment(to).subtract(count, 'minute'), moment(to)) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(1) + .fill(0) + .flatMap(() => + log + .create() + .dataset(degradedDatasetWithLimitsName) + .message('a log message') + .logLevel(MORE_THAN_1024_CHARS) + .service(serviceName) + .namespace(defaultNamespace) + .defaults({ + 'service.name': serviceName, + 'trace.id': generateShortId(), + test_field: [MORE_THAN_1024_CHARS, 'hello world'], + 'cloud.region': 'us-east-1', + }) + .timestamp(timestamp) + ); + }), + ]); + + // Rollover Datastream to reset the limit to default which is 1000 + await PageObjects.datasetQuality.rolloverDataStream(degradedDatasetWithLimitDataStreamName); + + // Ingest docs with 26 fields again + await synthtrace.index([ + timerange(moment(to).subtract(count, 'minute'), moment(to)) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(1) + .fill(0) + .flatMap(() => + log + .create() + .dataset(degradedDatasetWithLimitsName) + .message('a log message') + .logLevel(MORE_THAN_1024_CHARS) + .service(serviceName) + .namespace(defaultNamespace) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': serviceName, + 'trace.id': generateShortId(), + test_field: [MORE_THAN_1024_CHARS, 'hello world'], + 'cloud.region': 'us-east-1', + }) + .timestamp(timestamp) + ); + }), + ]); + }); + + describe('field character limit exceeded', () => { + it('should display cause as "field ignored" when a field is ignored due to field above issue', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'test_field', + }); + + await retry.tryForTime(5000, async () => { + const fieldIgnoredMessageExists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-cause', + 'field character limit exceeded' + ); + expect(fieldIgnoredMessageExists).to.be(true); + }); + + await PageObjects.datasetQuality.closeFlyout(); + }); + + it('should display values when cause is "field ignored"', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'test_field', + }); + + await retry.tryForTime(5000, async () => { + const testFieldValueExists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-values', + MORE_THAN_1024_CHARS + ); + expect(testFieldValueExists).to.be(true); + }); + + await PageObjects.datasetQuality.closeFlyout(); + }); + }); + + describe('field limit exceeded', () => { + it('should display cause as "field limit exceeded" when a field is ignored due to field limit issue', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'cloud', + }); + + await retry.tryForTime(5000, async () => { + const fieldLimitMessageExists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-cause', + 'field limit exceeded' + ); + expect(fieldLimitMessageExists).to.be(true); + }); + + await PageObjects.datasetQuality.closeFlyout(); + }); + + it('should display the limit when the cause is "field limit exceeded"', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'cloud', + }); + + await retry.tryForTime(5000, async () => { + const limitExists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-mappingLimit', + '25' + ); + expect(limitExists).to.be(true); + }); + + await PageObjects.datasetQuality.closeFlyout(); + }); + + it('should warn users about the issue not present in latest backing index', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'cloud', + }); + + await testSubjects.existOrFail( + PageObjects.datasetQuality.testSubjectSelectors + .datasetQualityDetailsDegradedFieldFlyoutIssueDoesNotExist + ); + }); + }); + + describe('current quality issues', () => { + it('should display issues only from latest backing index when current issues toggle is on', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + }); + + const currentIssuesToggleState = + await PageObjects.datasetQuality.getQualityIssueSwitchState(); + + expect(currentIssuesToggleState).to.be(false); + + const rows = + await PageObjects.datasetQuality.getDatasetQualityDetailsDegradedFieldTableRows(); + + expect(rows.length).to.eql(3); + + await testSubjects.click( + PageObjects.datasetQuality.testSubjectSelectors + .datasetQualityDetailsOverviewDegradedFieldToggleSwitch + ); + + const newCurrentIssuesToggleState = + await PageObjects.datasetQuality.getQualityIssueSwitchState(); + + expect(newCurrentIssuesToggleState).to.be(true); + + const newRows = + await PageObjects.datasetQuality.getDatasetQualityDetailsDegradedFieldTableRows(); + + expect(newRows.length).to.eql(2); + }); + + it('should keep the toggle on when url state says so', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'test_field', + showCurrentQualityIssues: true, + }); + + const currentIssuesToggleState = + await PageObjects.datasetQuality.getQualityIssueSwitchState(); + + expect(currentIssuesToggleState).to.be(true); + }); + + it('should display count from latest backing index when current issues toggle is on in the table and in the flyout', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'test_field', + showCurrentQualityIssues: true, + }); + + // Check value in Table + const table = await PageObjects.datasetQuality.parseDegradedFieldTable(); + const countColumn = table['Docs count']; + expect(await countColumn.getCellTexts()).to.eql(['5', '5']); + + // Check value in Flyout + await retry.tryForTime(5000, async () => { + const countValue = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldsList-docCount', + '5' + ); + expect(countValue).to.be(true); + }); + + // Toggle the switch + await testSubjects.click( + PageObjects.datasetQuality.testSubjectSelectors + .datasetQualityDetailsOverviewDegradedFieldToggleSwitch + ); + + // Check value in Table + const newTable = await PageObjects.datasetQuality.parseDegradedFieldTable(); + const newCountColumn = newTable['Docs count']; + expect(await newCountColumn.getCellTexts()).to.eql(['15', '15', '5']); + + // Check value in Flyout + await retry.tryForTime(5000, async () => { + const newCountValue = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldsList-docCount', + '15' + ); + expect(newCountValue).to.be(true); + }); + }); + + it('should close the flyout if passed value in URL no more exists in latest backing index and current quality toggle is switched on', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'cloud', + showCurrentQualityIssues: true, + }); + + await testSubjects.missingOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + }); + + it('should close the flyout when current quality switch is toggled on and the flyout is already open with an old field ', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'cloud', + }); + + await testSubjects.existOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + + await testSubjects.click( + PageObjects.datasetQuality.testSubjectSelectors + .datasetQualityDetailsOverviewDegradedFieldToggleSwitch + ); + + await testSubjects.missingOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + }); + }); + + after(async () => { + await synthtrace.clean(); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/dataset_quality/index.ts b/x-pack/test/functional/apps/dataset_quality/index.ts index 4d4db08d34c88..e9c497c4407ca 100644 --- a/x-pack/test/functional/apps/dataset_quality/index.ts +++ b/x-pack/test/functional/apps/dataset_quality/index.ts @@ -15,6 +15,6 @@ export default function ({ loadTestFile }: DatasetQualityFtrProviderContext) { loadTestFile(require.resolve('./dataset_quality_table_filters')); loadTestFile(require.resolve('./dataset_quality_privileges')); loadTestFile(require.resolve('./dataset_quality_details')); - loadTestFile(require.resolve('./dataset_quality_details_degraded_field_flyout')); + loadTestFile(require.resolve('./degraded_field_flyout')); }); } diff --git a/x-pack/test/functional/page_objects/dataset_quality.ts b/x-pack/test/functional/page_objects/dataset_quality.ts index 437aa6e2640d5..ccd48e220064a 100644 --- a/x-pack/test/functional/page_objects/dataset_quality.ts +++ b/x-pack/test/functional/page_objects/dataset_quality.ts @@ -9,6 +9,7 @@ import expect from '@kbn/expect'; import querystring from 'querystring'; import rison from '@kbn/rison'; import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services'; +import { IndicesIndexSettings } from '@elastic/elasticsearch/lib/api/types'; import { DATA_QUALITY_URL_STATE_KEY, datasetQualityUrlSchemaV1, @@ -77,6 +78,7 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv const euiSelectable = getService('selectable'); const find = getService('find'); const retry = getService('retry'); + const es = getService('es'); const selectors = { datasetQualityTable: '[data-test-subj="datasetQualityTable"]', @@ -132,6 +134,10 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv unifiedHistogramBreakdownSelectorSelectable: 'unifiedHistogramBreakdownSelectorSelectable', managementHome: 'managementHome', euiFlyoutCloseButton: 'euiFlyoutCloseButton', + datasetQualityDetailsDegradedFieldFlyoutIssueDoesNotExist: + 'datasetQualityDetailsDegradedFieldFlyoutIssueDoesNotExist', + datasetQualityDetailsOverviewDegradedFieldToggleSwitch: + 'datasetQualityDetailsOverviewDegradedFieldToggleSwitch', }; return { @@ -440,6 +446,27 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv return testSubjects.click(testSubjectSelectors.euiFlyoutCloseButton); }, + async setDataStreamSettings(name: string, settings: IndicesIndexSettings) { + return es.indices.putSettings({ + index: name, + settings, + }); + }, + + async rolloverDataStream(name: string) { + return es.indices.rollover({ + alias: name, + }); + }, + + async getQualityIssueSwitchState() { + const isSelected = await testSubjects.getAttribute( + testSubjectSelectors.datasetQualityDetailsOverviewDegradedFieldToggleSwitch, + 'aria-checked' + ); + return isSelected === 'true'; + }, + async parseTable(tableWrapper: WebElementWrapper, columnNamesOrIndexes: string[]) { const headerElementWrappers = await tableWrapper.findAllByCssSelector('thead th, thead td'); diff --git a/x-pack/test/functional_solution_sidenav/tests/observability_sidenav.ts b/x-pack/test/functional_solution_sidenav/tests/observability_sidenav.ts index c21844d851dce..87daa58fc2681 100644 --- a/x-pack/test/functional_solution_sidenav/tests/observability_sidenav.ts +++ b/x-pack/test/functional_solution_sidenav/tests/observability_sidenav.ts @@ -5,6 +5,7 @@ * 2.0. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getPageObjects, getService }: FtrProviderContext) { @@ -48,15 +49,35 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { deepLinkId: 'observabilityOnboarding', }); - // check the AI & ML subsection - await solutionNavigation.sidenav.openSection('observability_project_nav.aiMl'); // open AI & ML subsection - await solutionNavigation.sidenav.clickLink({ deepLinkId: 'ml:anomalyDetection' }); - await solutionNavigation.sidenav.expectLinkActive({ deepLinkId: 'ml:anomalyDetection' }); - await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'Anomaly Detection' }); + // open apm (Application) panel using the link button (not the button icon) + await solutionNavigation.sidenav.openPanel('apm', { button: 'link' }); + { + const isOpen = await solutionNavigation.sidenav.isPanelOpen('apm'); + expect(isOpen).to.be(true); + } + + await solutionNavigation.sidenav.closePanel('apm', { button: 'link' }); + { + const isOpen = await solutionNavigation.sidenav.isPanelOpen('apm'); + expect(isOpen).to.be(false); + } + + // open Infrastructure panel using the icon button and navigate to some link inside the panel + await solutionNavigation.sidenav.openPanel('metrics', { button: 'icon' }); + { + const isOpen = await solutionNavigation.sidenav.isPanelOpen('metrics'); + expect(isOpen).to.be(true); + } + await solutionNavigation.sidenav.clickPanelLink('metrics:inventory'); await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ - deepLinkId: 'ml:anomalyDetection', + text: 'Infrastructure inventory', }); + { + const isOpen = await solutionNavigation.sidenav.isPanelOpen('metrics'); + expect(isOpen).to.be(false); + } + // navigate to a different section await solutionNavigation.sidenav.openSection('project_settings_project_nav'); await solutionNavigation.sidenav.clickLink({ deepLinkId: 'management' }); diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts index 96256248e5d88..f061fe68649d5 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -26,5 +26,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./pages/rule_details_page')); loadTestFile(require.resolve('./pages/alert_details_page')); loadTestFile(require.resolve('./pages/alerts/metric_threshold')); + loadTestFile(require.resolve('./sidenav/sidenav')); }); } diff --git a/x-pack/test/observability_functional/apps/observability/sidenav/sidenav.ts b/x-pack/test/observability_functional/apps/observability/sidenav/sidenav.ts new file mode 100644 index 0000000000000..201729b0bcc06 --- /dev/null +++ b/x-pack/test/observability_functional/apps/observability/sidenav/sidenav.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const { common, solutionNavigation } = getPageObjects(['common', 'solutionNavigation']); + const spaces = getService('spaces'); + const browser = getService('browser'); + + describe('o11y sidenav', () => { + let cleanUp: () => Promise; + let spaceCreated: { id: string } = { id: '' }; + before(async () => { + // Navigate to the spaces management page which will log us in Kibana + await common.navigateToUrl('management', 'kibana/spaces', { + shouldUseHashForSubUrl: false, + }); + // Create a space with the observability solution and navigate to its home page + ({ cleanUp, space: spaceCreated } = await spaces.create({ solution: 'oblt' })); + await browser.navigateTo(spaces.getRootUrl(spaceCreated.id)); + }); + + after(async () => { + // Clean up space created + await cleanUp(); + }); + + describe('sidenav & breadcrumbs', () => { + it('renders the correct nav and navigate to links', async () => { + await solutionNavigation.sidenav.clickLink({ navId: 'observabilityAIAssistant' }); // click on AI Assistant link + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ text: 'AI Assistant' }); + + // check Other Tools section + await solutionNavigation.sidenav.openPanel('otherTools', { button: 'link' }); + { + const isOpen = await solutionNavigation.sidenav.isPanelOpen('otherTools'); + expect(isOpen).to.be(true); + } + await solutionNavigation.sidenav.expectLinkExists({ + panelNavLinkId: 'logs:anomalies', + }); + + await solutionNavigation.sidenav.expectLinkExists({ + panelNavLinkId: 'logs:log-categories', + }); + + await solutionNavigation.sidenav.clickPanelLink('visualize'); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + text: 'Visualize library', + }); + + // check Machine Learning section + await solutionNavigation.sidenav.openPanel('machine_learning-landing'); + { + const isOpen = await solutionNavigation.sidenav.isPanelOpen('machine_learning-landing'); + expect(isOpen).to.be(true); + } + + await solutionNavigation.sidenav.clickPanelLink('ml:suppliedConfigurations'); + await solutionNavigation.breadcrumbs.expectBreadcrumbExists({ + text: 'Supplied configurations', + }); + }); + }); + }); +} diff --git a/x-pack/test/observability_functional/with_rac_write.config.ts b/x-pack/test/observability_functional/with_rac_write.config.ts index 83227af64d701..6ca4262b3ea42 100644 --- a/x-pack/test/observability_functional/with_rac_write.config.ts +++ b/x-pack/test/observability_functional/with_rac_write.config.ts @@ -52,6 +52,9 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalConfig.get('kbnTestServer'), serverArgs: [ ...xpackFunctionalConfig.get('kbnTestServer.serverArgs'), + '--xpack.cloud.id=ftr_fake_cloud_id:aGVsbG8uY29tOjQ0MyRFUzEyM2FiYyRrYm4xMjNhYmM=', + '--xpack.cloud.base_url=https://cloud.elastic.co', + '--xpack.spaces.allowSolutionVisibility=true', `--elasticsearch.hosts=https://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, `--plugin-path=${resolve(__dirname, '../functional_with_es_ssl/plugins/alerts')}`, diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts index 3727c2ccbd501..0045a79ff4394 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/esql_rule.cy.ts @@ -70,7 +70,7 @@ const workaroundForResizeObserver = () => describe( 'Detection ES|QL rules, creation', { - tags: ['@ess', '@serverless'], + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], }, () => { const rule = getEsqlRule(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/threshold_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/threshold_rule.cy.ts index 3243a2974836f..11740c1f795f8 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/threshold_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_creation/threshold_rule.cy.ts @@ -68,7 +68,7 @@ import { CREATE_RULE_URL } from '../../../../urls/navigation'; describe( 'Threshold rules', { - tags: ['@ess', '@serverless'], + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], }, () => { const rule = getNewThresholdRule(); diff --git a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts index 0251a7e935903..e89e4b6afb817 100644 --- a/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts +++ b/x-pack/test/security_solution_cypress/cypress/e2e/detection_response/detection_engine/rule_edit/machine_learning_rule.cy.ts @@ -42,7 +42,7 @@ import { RULES_MANAGEMENT_URL } from '../../../../urls/rules_management'; describe( 'Machine Learning Detection Rules - Editing', { - tags: ['@ess', '@serverless'], + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], }, () => { let mlRule: ReturnType; diff --git a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json index 01d4b47230294..fcf6043a30012 100644 --- a/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json +++ b/x-pack/test/spaces_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json @@ -42,6 +42,29 @@ } } +{ + "type": "_doc", + "value": { + "id": "space:space_3", + "index": ".kibana", + "source": { + "space": { + "description": "This is the third test space", + "solution": "es", + "disabledFeatures": [], + "name": "Space 3" + }, + "type": "space", + "updated_at": "2017-09-21T18:49:16.270Z", + "coreMigrationVersion": "8.8.0", + "typeMigrationVersion": "6.6.0", + "managed": false, + "references": [] + }, + "type": "_doc" + } +} + { "type": "_doc", "value": { diff --git a/x-pack/test/spaces_api_integration/common/lib/authentication.ts b/x-pack/test/spaces_api_integration/common/lib/authentication.ts index 6e76e46dc5ee0..27f644c3f5cd5 100644 --- a/x-pack/test/spaces_api_integration/common/lib/authentication.ts +++ b/x-pack/test/spaces_api_integration/common/lib/authentication.ts @@ -66,6 +66,14 @@ export const AUTHENTICATION = { username: 'a_kibana_rbac_space_1_2_read_user', password: 'password', }, + KIBANA_RBAC_SPACE_3_ALL_USER: { + username: 'a_kibana_rbac_space_3_all_user', + password: 'password', + }, + KIBANA_RBAC_SPACE_3_READ_USER: { + username: 'a_kibana_rbac_space_3_read_user', + password: 'password', + }, KIBANA_RBAC_DEFAULT_SPACE_SAVED_OBJECTS_ALL_USER: { username: 'a_kibana_rbac_default_space_saved_objects_all_user', password: 'password', diff --git a/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts b/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts index 58ef5ba9f9481..b66c4a02a5bd6 100644 --- a/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts +++ b/x-pack/test/spaces_api_integration/common/lib/create_users_and_roles.ts @@ -184,6 +184,30 @@ export const createUsersAndRoles = async (es: Client, supertest: SuperTestAgent) }) .expect(204); + await supertest + .put('/api/security/role/kibana_rbac_space_3_all_user') + .send({ + kibana: [ + { + base: ['all'], + spaces: ['space_3'], + }, + ], + }) + .expect(204); + + await supertest + .put('/api/security/role/kibana_rbac_space_3_read_user') + .send({ + kibana: [ + { + base: ['read'], + spaces: ['space_3'], + }, + ], + }) + .expect(204); + await supertest .put('/api/security/role/kibana_rbac_default_space_saved_objects_all_user') .send({ @@ -344,6 +368,26 @@ export const createUsersAndRoles = async (es: Client, supertest: SuperTestAgent) }, }); + await es.security.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_SPACE_3_ALL_USER.username, + body: { + password: AUTHENTICATION.KIBANA_RBAC_SPACE_3_ALL_USER.password, + roles: ['kibana_rbac_space_3_all_user'], + full_name: 'a kibana rbac space 3 all user', + email: 'a_kibana_rbac_space_3_all_user@elastic.co', + }, + }); + + await es.security.putUser({ + username: AUTHENTICATION.KIBANA_RBAC_SPACE_3_READ_USER.username, + body: { + password: AUTHENTICATION.KIBANA_RBAC_SPACE_3_READ_USER.password, + roles: ['kibana_rbac_space_3_read_user'], + full_name: 'a kibana rbac space 3 read-only user', + email: 'a_kibana_rbac_space_3_readonly_user@elastic.co', + }, + }); + await es.security.putUser({ username: AUTHENTICATION.KIBANA_RBAC_SPACE_2_ALL_USER.username, body: { diff --git a/x-pack/test/spaces_api_integration/common/lib/spaces.ts b/x-pack/test/spaces_api_integration/common/lib/spaces.ts index 75a1e518452b9..c61b7646233e7 100644 --- a/x-pack/test/spaces_api_integration/common/lib/spaces.ts +++ b/x-pack/test/spaces_api_integration/common/lib/spaces.ts @@ -12,6 +12,9 @@ export const SPACES = { SPACE_2: { spaceId: 'space_2', }, + SPACE_3: { + spaceId: 'space_3', + }, DEFAULT: { spaceId: 'default', }, diff --git a/x-pack/test/spaces_api_integration/common/suites/create.ts b/x-pack/test/spaces_api_integration/common/suites/create.ts index fc2bd1b841ccc..ce3113ec9639c 100644 --- a/x-pack/test/spaces_api_integration/common/suites/create.ts +++ b/x-pack/test/spaces_api_integration/common/suites/create.ts @@ -65,14 +65,32 @@ export function createTestSuiteFactory(esArchiver: any, supertest: SuperTest) => { - expect(resp.body).to.eql({ + const disabledFeatures = resp.body.disabledFeatures.sort(); + + const expected = { id: 'solution', name: 'space with solution', description: 'a description', color: '#5c5959', - disabledFeatures: [], + disabledFeatures: [ + // Disabled features are automatically added to the space when a solution is set + 'apm', + 'infrastructure', + 'inventory', + 'logs', + 'observabilityAIAssistant', + 'observabilityCases', + 'securitySolutionAssistant', + 'securitySolutionAttackDiscovery', + 'securitySolutionCases', + 'siem', + 'slo', + 'uptime', + ], solution: 'es', - }); + }; + + expect({ ...resp.body, disabledFeatures }).to.eql(expected); }; const makeCreateTest = diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index b1ad6241e11df..fd04b79fd1ef5 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -62,14 +62,14 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S const expectedBuckets = [ { key: 'default', - doc_count: 9, + doc_count: 10, countByType: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ + { key: 'space', doc_count: 3 }, // since space objects are namespace-agnostic, they appear in the "default" agg bucket { key: 'visualization', doc_count: 3 }, { key: 'legacy-url-alias', doc_count: 2 }, // aliases (1) - { key: 'space', doc_count: 2 }, // since space objects are namespace-agnostic, they appear in the "default" agg bucket { key: 'dashboard', doc_count: 1 }, { key: 'index-pattern', doc_count: 1 }, ], @@ -182,7 +182,7 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S describe(`when the space doesn't exist`, () => { it(`should return ${tests.doesntExist.statusCode} ${scenario}`, async () => { return supertest - .delete(`${urlPrefix}/api/spaces/space/space_3`) + .delete(`${urlPrefix}/api/spaces/space/space_7`) .auth(user.username, user.password) .expect(tests.doesntExist.statusCode) .then(tests.doesntExist.response); diff --git a/x-pack/test/spaces_api_integration/common/suites/get.ts b/x-pack/test/spaces_api_integration/common/suites/get.ts index 6956599ae40a6..a733bd21c5fc2 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get.ts @@ -71,8 +71,37 @@ export function getTestSuiteFactory(esArchiver: any, supertest: SuperAgent) description: 'This is the second test space', disabledFeatures: [], }, + { + id: 'space_3', + name: 'Space 3', + description: 'This is the third test space', + solution: 'es', + disabledFeatures: [ + // Disabled features are automatically added to the space when a solution is set + 'apm', + 'infrastructure', + 'inventory', + 'logs', + 'observabilityAIAssistant', + 'observabilityCases', + 'securitySolutionAssistant', + 'securitySolutionAttackDiscovery', + 'securitySolutionCases', + 'siem', + 'slo', + 'uptime', + ], + }, ]; - expect(resp.body).to.eql(allSpaces.find((space) => space.id === spaceId)); + + const disabledFeatures = (resp.body.disabledFeatures ?? []).sort(); + + const expectedSpace = allSpaces.find((space) => space.id === spaceId); + if (expectedSpace) { + expectedSpace.disabledFeatures.sort(); + } + + expect({ ...resp.body, disabledFeatures }).to.eql(expectedSpace); }; const makeGetTest = diff --git a/x-pack/test/spaces_api_integration/common/suites/get_all.ts b/x-pack/test/spaces_api_integration/common/suites/get_all.ts index 5452624f77648..88625c3d9b51e 100644 --- a/x-pack/test/spaces_api_integration/common/suites/get_all.ts +++ b/x-pack/test/spaces_api_integration/common/suites/get_all.ts @@ -35,7 +35,17 @@ interface AuthorizedPurposes { shareSavedObjectsIntoSpace: boolean; } -const ALL_SPACE_RESULTS = [ +interface Space { + id: string; + name: string; + color?: string; + description: string; + solution?: string; + _reserved?: boolean; + disabledFeatures: string[]; +} + +const ALL_SPACE_RESULTS: Space[] = [ { id: 'default', name: 'Default', @@ -56,14 +66,42 @@ const ALL_SPACE_RESULTS = [ description: 'This is the second test space', disabledFeatures: [], }, + { + id: 'space_3', + name: 'Space 3', + description: 'This is the third test space', + solution: 'es', + disabledFeatures: [ + // Disabled features are automatically added to the space when a solution is set + 'apm', + 'infrastructure', + 'inventory', + 'logs', + 'observabilityAIAssistant', + 'observabilityCases', + 'securitySolutionAssistant', + 'securitySolutionAttackDiscovery', + 'securitySolutionCases', + 'siem', + 'slo', + 'uptime', + ], + }, ]; +const sortDisabledFeatures = (space: Space) => { + return { + ...space, + disabledFeatures: [...space.disabledFeatures].sort(), + }; +}; + export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const createExpectResults = (...spaceIds: string[]) => (resp: { [key: string]: any }) => { const expectedBody = ALL_SPACE_RESULTS.filter((entry) => spaceIds.includes(entry.id)); - expect(resp.body).to.eql(expectedBody); + expect(resp.body.map(sortDisabledFeatures)).to.eql(expectedBody.map(sortDisabledFeatures)); }; const createExpectAllPurposesResults = @@ -72,7 +110,7 @@ export function getAllTestSuiteFactory(esArchiver: any, supertest: SuperTest spaceIds.includes(entry.id)).map( (x) => ({ ...x, authorizedPurposes }) ); - expect(resp.body).to.eql(expectedBody); + expect(resp.body.map(sortDisabledFeatures)).to.eql(expectedBody.map(sortDisabledFeatures)); }; const expectEmptyResult = (resp: { [key: string]: any }) => { diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts index 122a9218555fe..7354138b7987e 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get.ts @@ -58,6 +58,22 @@ export default function getSpaceTestSuite({ getService }: FtrProviderContext) { dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, }, }, + { + spaceId: SPACES.SPACE_3.spaceId, // This space has a solution set and we expect disabledFeatures to be automatically set + otherSpaceId: SPACES.DEFAULT.spaceId, + users: { + noAccess: AUTHENTICATION.NOT_A_KIBANA_USER, + superuser: AUTHENTICATION.SUPERUSER, + allGlobally: AUTHENTICATION.KIBANA_RBAC_USER, + readGlobally: AUTHENTICATION.KIBANA_RBAC_DASHBOARD_ONLY_USER, + allAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_3_ALL_USER, + readAtSpace: AUTHENTICATION.KIBANA_RBAC_SPACE_3_READ_USER, + allAtOtherSpace: AUTHENTICATION.KIBANA_RBAC_DEFAULT_SPACE_ALL_USER, + legacyAll: AUTHENTICATION.KIBANA_LEGACY_USER, + dualAll: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_USER, + dualRead: AUTHENTICATION.KIBANA_DUAL_PRIVILEGES_DASHBOARD_ONLY_USER, + }, + }, ].forEach((scenario) => { getTest(`user with no access`, { currentSpaceId: scenario.spaceId, diff --git a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts index 992ab6c7028a6..d2c3b8be03be2 100644 --- a/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts +++ b/x-pack/test/spaces_api_integration/security_and_spaces/apis/get_all.ts @@ -119,15 +119,15 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext tests: { exists: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, copySavedObjectsPurpose: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, shareSavedObjectsPurpose: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, includeAuthorizedPurposes: { statusCode: 200, @@ -135,7 +135,8 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext authorizedAll, 'default', 'space_1', - 'space_2' + 'space_2', + 'space_3' ), }, }, @@ -147,15 +148,15 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext tests: { exists: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, copySavedObjectsPurpose: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, shareSavedObjectsPurpose: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, includeAuthorizedPurposes: { statusCode: 200, @@ -163,7 +164,8 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext authorizedAll, 'default', 'space_1', - 'space_2' + 'space_2', + 'space_3' ), }, }, @@ -175,15 +177,15 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext tests: { exists: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, copySavedObjectsPurpose: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, shareSavedObjectsPurpose: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, includeAuthorizedPurposes: { statusCode: 200, @@ -191,7 +193,8 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext authorizedAll, 'default', 'space_1', - 'space_2' + 'space_2', + 'space_3' ), }, }, @@ -226,7 +229,7 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext tests: { exists: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, copySavedObjectsPurpose: { statusCode: 403, @@ -242,7 +245,8 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext authorizedRead, 'default', 'space_1', - 'space_2' + 'space_2', + 'space_3' ), }, }, @@ -254,7 +258,7 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext tests: { exists: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, copySavedObjectsPurpose: { statusCode: 403, @@ -270,7 +274,8 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext authorizedRead, 'default', 'space_1', - 'space_2' + 'space_2', + 'space_3' ), }, }, diff --git a/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts b/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts index 46ddb59461945..6331c843649fa 100644 --- a/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts +++ b/x-pack/test/spaces_api_integration/spaces_only/apis/get_all.ts @@ -34,19 +34,19 @@ export default function getAllSpacesTestSuite({ getService }: FtrProviderContext tests: { exists: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, copySavedObjectsPurpose: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, shareSavedObjectsPurpose: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, includeAuthorizedPurposes: { statusCode: 200, - response: createExpectResults('default', 'space_1', 'space_2'), + response: createExpectResults('default', 'space_1', 'space_2', 'space_3'), }, }, }); diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/data_stream_details.ts b/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/data_stream_details.ts index 4b4bb77b8f59f..f6028c11f734a 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/data_stream_details.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/data_stream_details.ts @@ -100,7 +100,7 @@ export default function ({ getService }: DatasetQualityFtrContextProvider) { it('returns "sizeBytes" correctly', async () => { // Metering stats api is cached and refreshed every 30 seconds - await retry.waitForWithTimeout('Metering stats cache is refreshed', 31000, async () => { + await retry.waitForWithTimeout('Metering stats cache is refreshed', 45000, async () => { const detailsResponse = await callApi( `${type}-${dataset}-${namespace}`, roleAuthc, diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/data_stream_settings.ts b/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/data_stream_settings.ts index 52a1c51d24917..a132bc01c9720 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/data_stream_settings.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/data_stream_settings.ts @@ -14,6 +14,7 @@ import { DatasetQualityApiError, } from './common/dataset_quality_api_supertest'; import { DatasetQualityFtrContextProvider } from './common/services'; +import { createBackingIndexNameWithoutVersion } from './utils'; export default function ({ getService }: DatasetQualityFtrContextProvider) { const datasetQualityApiClient: DatasetQualityApiClient = getService('datasetQualityApiClient'); @@ -97,16 +98,23 @@ export default function ({ getService }: DatasetQualityFtrContextProvider) { expect(resp.body).eql(defaultDataStreamPrivileges); }); - it('returns "createdOn" correctly', async () => { + it('returns "createdOn" and "lastBackingIndexName" correctly', async () => { const dataStreamSettings = await getDataStreamSettingsOfEarliestIndex( esClient, `${type}-${dataset}-${namespace}` ); const resp = await callApi(`${type}-${dataset}-${namespace}`, roleAuthc, internalReqHeader); expect(resp.body.createdOn).to.be(Number(dataStreamSettings?.index?.creation_date)); + expect(resp.body.lastBackingIndexName).to.be( + `${createBackingIndexNameWithoutVersion({ + type, + dataset, + namespace, + })}-000001` + ); }); - it('returns "createdOn" correctly for rolled over dataStream', async () => { + it('returns "createdOn" and "lastBackingIndexName" correctly for rolled over dataStream', async () => { await rolloverDataStream(esClient, `${type}-${dataset}-${namespace}`); const dataStreamSettings = await getDataStreamSettingsOfEarliestIndex( esClient, @@ -114,6 +122,9 @@ export default function ({ getService }: DatasetQualityFtrContextProvider) { ); const resp = await callApi(`${type}-${dataset}-${namespace}`, roleAuthc, internalReqHeader); expect(resp.body.createdOn).to.be(Number(dataStreamSettings?.index?.creation_date)); + expect(resp.body.lastBackingIndexName).to.be( + `${createBackingIndexNameWithoutVersion({ type, dataset, namespace })}-000002` + ); }); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/utils/data_stream.ts b/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/utils/data_stream.ts index bdf5187db0725..6fb1b043b5ce5 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/utils/data_stream.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/dataset_quality_api_integration/utils/data_stream.ts @@ -6,6 +6,7 @@ */ import { Client } from '@elastic/elasticsearch'; +import { IndicesIndexSettings } from '@elastic/elasticsearch/lib/api/types'; export async function rolloverDataStream(es: Client, name: string) { return es.indices.rollover({ alias: name }); @@ -24,3 +25,35 @@ export async function getDataStreamSettingsOfEarliestIndex(es: Client, name: str return matchingIndexesObj[matchingIndexes[0]].settings; } + +function getCurrentDateFormatted() { + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}.${month}.${day}`; +} + +export function createBackingIndexNameWithoutVersion({ + type, + dataset, + namespace = 'default', +}: { + type: string; + dataset: string; + namespace: string; +}) { + return `.ds-${type}-${dataset}-${namespace}-${getCurrentDateFormatted()}`; +} + +export async function setDataStreamSettings( + esClient: Client, + name: string, + settings: IndicesIndexSettings +) { + return esClient.indices.putSettings({ + index: name, + settings, + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts index ecf6ddd2c76f8..ac53ceacf9a0a 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/platform_security/authorization.ts @@ -231,10 +231,12 @@ export default function ({ getService }: FtrProviderContext) { "api:infra", "app:infra", "app:logs", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:navLinks/infra", "ui:navLinks/logs", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-ui-source/bulk_get", "saved_object:infrastructure-ui-source/get", "saved_object:infrastructure-ui-source/find", @@ -881,10 +883,12 @@ export default function ({ getService }: FtrProviderContext) { "api:infra", "app:infra", "app:logs", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:navLinks/infra", "ui:navLinks/logs", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-ui-source/bulk_get", "saved_object:infrastructure-ui-source/get", "saved_object:infrastructure-ui-source/find", @@ -1441,10 +1445,12 @@ export default function ({ getService }: FtrProviderContext) { "api:infra", "app:infra", "app:logs", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:navLinks/infra", "ui:navLinks/logs", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-ui-source/bulk_get", "saved_object:infrastructure-ui-source/get", "saved_object:infrastructure-ui-source/find", @@ -1724,10 +1730,12 @@ export default function ({ getService }: FtrProviderContext) { "api:infra", "app:infra", "app:logs", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:navLinks/infra", "ui:navLinks/logs", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-ui-source/bulk_get", "saved_object:infrastructure-ui-source/get", "saved_object:infrastructure-ui-source/find", @@ -3820,12 +3828,14 @@ export default function ({ getService }: FtrProviderContext) { "app:infra", "app:logs", "app:kibana", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:management/insightsAndAlerting/triggersActions", "ui:navLinks/infra", "ui:navLinks/logs", "ui:navLinks/kibana", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-ui-source/bulk_get", "saved_object:infrastructure-ui-source/get", "saved_object:infrastructure-ui-source/find", @@ -4171,12 +4181,14 @@ export default function ({ getService }: FtrProviderContext) { "app:infra", "app:logs", "app:kibana", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:management/insightsAndAlerting/triggersActions", "ui:navLinks/infra", "ui:navLinks/logs", "ui:navLinks/kibana", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-ui-source/bulk_get", "saved_object:infrastructure-ui-source/get", "saved_object:infrastructure-ui-source/find", @@ -4429,12 +4441,14 @@ export default function ({ getService }: FtrProviderContext) { "app:infra", "app:logs", "app:kibana", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:management/insightsAndAlerting/triggersActions", "ui:navLinks/infra", "ui:navLinks/logs", "ui:navLinks/kibana", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-ui-source/bulk_get", "saved_object:infrastructure-ui-source/get", "saved_object:infrastructure-ui-source/find", @@ -4591,12 +4605,14 @@ export default function ({ getService }: FtrProviderContext) { "app:infra", "app:logs", "app:kibana", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:management/insightsAndAlerting/triggersActions", "ui:navLinks/infra", "ui:navLinks/logs", "ui:navLinks/kibana", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-ui-source/bulk_get", "saved_object:infrastructure-ui-source/get", "saved_object:infrastructure-ui-source/find", @@ -4901,9 +4917,11 @@ export default function ({ getService }: FtrProviderContext) { "alerting:xpack.ml.anomaly_detection_alert/infrastructure/alert/getAlertSummary", "alerting:xpack.ml.anomaly_detection_alert/infrastructure/alert/update", "app:logs", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:navLinks/logs", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-monitoring-log-view/bulk_get", "saved_object:infrastructure-monitoring-log-view/get", "saved_object:infrastructure-monitoring-log-view/find", @@ -5592,9 +5610,11 @@ export default function ({ getService }: FtrProviderContext) { "alerting:xpack.ml.anomaly_detection_alert/infrastructure/alert/getAlertSummary", "alerting:xpack.ml.anomaly_detection_alert/infrastructure/alert/update", "app:logs", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:navLinks/logs", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-monitoring-log-view/bulk_get", "saved_object:infrastructure-monitoring-log-view/get", "saved_object:infrastructure-monitoring-log-view/find", @@ -6160,9 +6180,11 @@ export default function ({ getService }: FtrProviderContext) { "alerting:xpack.ml.anomaly_detection_alert/infrastructure/alert/getAuthorizedAlertsIndices", "alerting:xpack.ml.anomaly_detection_alert/infrastructure/alert/getAlertSummary", "app:logs", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:navLinks/logs", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-monitoring-log-view/bulk_get", "saved_object:infrastructure-monitoring-log-view/get", "saved_object:infrastructure-monitoring-log-view/find", @@ -6458,9 +6480,11 @@ export default function ({ getService }: FtrProviderContext) { "alerting:xpack.ml.anomaly_detection_alert/infrastructure/alert/getAuthorizedAlertsIndices", "alerting:xpack.ml.anomaly_detection_alert/infrastructure/alert/getAlertSummary", "app:logs", + "app:observability-logs-explorer", "ui:catalogue/infralogging", "ui:catalogue/logs", "ui:navLinks/logs", + "ui:navLinks/observability-logs-explorer", "saved_object:infrastructure-monitoring-log-view/bulk_get", "saved_object:infrastructure-monitoring-log-view/get", "saved_object:infrastructure-monitoring-log-view/find", diff --git a/x-pack/test_serverless/api_integration/test_suites/security/config.ts b/x-pack/test_serverless/api_integration/test_suites/security/config.ts index b97ed38960561..d40cde3c25837 100644 --- a/x-pack/test_serverless/api_integration/test_suites/security/config.ts +++ b/x-pack/test_serverless/api_integration/test_suites/security/config.ts @@ -24,6 +24,6 @@ export default createTestConfig({ // useful for testing (also enabled in MKI QA) '--coreApp.allowDynamicConfigOverrides=true', `--xpack.securitySolutionServerless.cloudSecurityUsageReportingTaskInterval=5s`, - `--xpack.securitySolutionServerless.usageReportingApiUrl=http://localhost:8081/api/v1/usage`, + `--xpack.securitySolutionServerless.usageApi.url=http://localhost:8081/api/v1/usage`, ], }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/dataset_quality_details.ts b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/dataset_quality_details.ts index 477e45d0e59f9..712ed11a28f93 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/dataset_quality_details.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/dataset_quality_details.ts @@ -56,7 +56,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const degradedDatasetName = datasetNames[2]; const degradedDataStreamName = `logs-${degradedDatasetName}-${defaultNamespace}`; - describe('Flyout', function () { + describe('Dataset quality details', function () { before(async () => { // Install Apache Integration and ingest logs for it await PageObjects.observabilityLogsExplorer.installPackage(apachePkg); @@ -178,7 +178,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(parseInt(degradedDocs, 10)).to.be(1); expect(parseInt(services, 10)).to.be(3); expect(parseInt(hosts, 10)).to.be(52); - expect(parseInt(size, 10)).to.be.greaterThan(0); + // metering stats API is cached for 30seconds, waiting for the exact value is not optimal in this case + // rather we can just check if any value is present + expect(size).to.be.ok(); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/dataset_quality_details_degraded_field_flyout.ts b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/dataset_quality_details_degraded_field_flyout.ts deleted file mode 100644 index 86632ec5a4bfc..0000000000000 --- a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/dataset_quality_details_degraded_field_flyout.ts +++ /dev/null @@ -1,106 +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 expect from '@kbn/expect'; -import { - createDegradedFieldsRecord, - datasetNames, - defaultNamespace, - getInitialTestLogs, - ANOTHER_1024_CHARS, - MORE_THAN_1024_CHARS, -} from './data'; -import { FtrProviderContext } from '../../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects([ - 'common', - 'navigationalSearch', - 'observabilityLogsExplorer', - 'datasetQuality', - 'svlCommonPage', - ]); - const testSubjects = getService('testSubjects'); - const synthtrace = getService('svlLogsSynthtraceClient'); - const retry = getService('retry'); - const to = '2024-01-01T12:00:00.000Z'; - const degradedDatasetName = datasetNames[2]; - const degradedDataStreamName = `logs-${degradedDatasetName}-${defaultNamespace}`; - - describe('Degraded fields flyout', () => { - before(async () => { - await synthtrace.index([ - // Ingest basic logs - getInitialTestLogs({ to, count: 4 }), - // Ingest Degraded Logs - createDegradedFieldsRecord({ - to: new Date().toISOString(), - count: 2, - dataset: degradedDatasetName, - }), - ]); - await PageObjects.svlCommonPage.loginWithPrivilegedRole(); - }); - - after(async () => { - await synthtrace.clean(); - }); - - describe('degraded field flyout open-close', () => { - it('should open and close the flyout when user clicks on the expand button', async () => { - await PageObjects.datasetQuality.navigateToDetails({ - dataStream: degradedDataStreamName, - }); - - await PageObjects.datasetQuality.openDegradedFieldFlyout('test_field'); - - await testSubjects.existOrFail( - PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout - ); - - await PageObjects.datasetQuality.closeFlyout(); - }); - - it('should open the flyout when navigating to the page with degradedField in URL State', async () => { - await PageObjects.datasetQuality.navigateToDetails({ - dataStream: degradedDataStreamName, - expandedDegradedField: 'test_field', - }); - - await testSubjects.existOrFail( - PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout - ); - - await PageObjects.datasetQuality.closeFlyout(); - }); - }); - - describe('values exist', () => { - it('should display the degraded field values', async () => { - await PageObjects.datasetQuality.navigateToDetails({ - dataStream: degradedDataStreamName, - expandedDegradedField: 'test_field', - }); - - await retry.tryForTime(5000, async () => { - const cloudAvailabilityZoneValueExists = await PageObjects.datasetQuality.doesTextExist( - 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-values', - ANOTHER_1024_CHARS - ); - const cloudAvailabilityZoneValue2Exists = await PageObjects.datasetQuality.doesTextExist( - 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-values', - MORE_THAN_1024_CHARS - ); - expect(cloudAvailabilityZoneValueExists).to.be(true); - expect(cloudAvailabilityZoneValue2Exists).to.be(true); - }); - - await PageObjects.datasetQuality.closeFlyout(); - }); - }); - }); -} diff --git a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts new file mode 100644 index 0000000000000..263dc8652ad75 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/degraded_field_flyout.ts @@ -0,0 +1,417 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import moment from 'moment'; +import { generateShortId, log, timerange } from '@kbn/apm-synthtrace-client'; +import { + createDegradedFieldsRecord, + datasetNames, + defaultNamespace, + getInitialTestLogs, + ANOTHER_1024_CHARS, + MORE_THAN_1024_CHARS, +} from './data'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects([ + 'common', + 'navigationalSearch', + 'observabilityLogsExplorer', + 'datasetQuality', + 'svlCommonPage', + ]); + const testSubjects = getService('testSubjects'); + const synthtrace = getService('svlLogsSynthtraceClient'); + const retry = getService('retry'); + const to = new Date().toISOString(); + const degradedDatasetName = datasetNames[2]; + const degradedDataStreamName = `logs-${degradedDatasetName}-${defaultNamespace}`; + + const degradedDatasetWithLimitsName = 'degraded.dataset.rca'; + const degradedDatasetWithLimitDataStreamName = `logs-${degradedDatasetWithLimitsName}-${defaultNamespace}`; + const serviceName = 'test_service'; + const count = 5; + + describe('Degraded fields flyout', () => { + before(async () => { + await synthtrace.index([ + // Ingest basic logs + getInitialTestLogs({ to, count: 4 }), + // Ingest Degraded Logs + createDegradedFieldsRecord({ + to: new Date().toISOString(), + count: 2, + dataset: degradedDatasetName, + }), + ]); + await PageObjects.svlCommonPage.loginWithPrivilegedRole(); + }); + + after(async () => { + await synthtrace.clean(); + }); + + describe('degraded field flyout open-close', () => { + it('should open and close the flyout when user clicks on the expand button', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDataStreamName, + }); + + await PageObjects.datasetQuality.openDegradedFieldFlyout('test_field'); + + await testSubjects.existOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + + await PageObjects.datasetQuality.closeFlyout(); + }); + + it('should open the flyout when navigating to the page with degradedField in URL State', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDataStreamName, + expandedDegradedField: 'test_field', + }); + + await testSubjects.existOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + + await PageObjects.datasetQuality.closeFlyout(); + }); + }); + + describe('values exist', () => { + it('should display the degraded field values', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDataStreamName, + expandedDegradedField: 'test_field', + }); + + await retry.tryForTime(5000, async () => { + const cloudAvailabilityZoneValueExists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-values', + ANOTHER_1024_CHARS + ); + const cloudAvailabilityZoneValue2Exists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-values', + MORE_THAN_1024_CHARS + ); + expect(cloudAvailabilityZoneValueExists).to.be(true); + expect(cloudAvailabilityZoneValue2Exists).to.be(true); + }); + + await PageObjects.datasetQuality.closeFlyout(); + }); + }); + + describe('testing root cause for ignored fields', () => { + before(async () => { + // Ingest Degraded Logs with 25 fields + await synthtrace.index([ + timerange(moment(to).subtract(count, 'minute'), moment(to)) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(1) + .fill(0) + .flatMap(() => + log + .create() + .dataset(degradedDatasetWithLimitsName) + .message('a log message') + .logLevel(MORE_THAN_1024_CHARS) + .service(serviceName) + .namespace(defaultNamespace) + .defaults({ + 'service.name': serviceName, + 'trace.id': generateShortId(), + test_field: [MORE_THAN_1024_CHARS, 'hello world'], + }) + .timestamp(timestamp) + ); + }), + ]); + + // Set Limit of 25 + await PageObjects.datasetQuality.setDataStreamSettings( + degradedDatasetWithLimitDataStreamName, + { + 'mapping.total_fields.limit': 25, + } + ); + + // Ingest Degraded Logs with 26 field + await synthtrace.index([ + timerange(moment(to).subtract(count, 'minute'), moment(to)) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(1) + .fill(0) + .flatMap(() => + log + .create() + .dataset(degradedDatasetWithLimitsName) + .message('a log message') + .logLevel(MORE_THAN_1024_CHARS) + .service(serviceName) + .namespace(defaultNamespace) + .defaults({ + 'service.name': serviceName, + 'trace.id': generateShortId(), + test_field: [MORE_THAN_1024_CHARS, 'hello world'], + 'cloud.region': 'us-east-1', + }) + .timestamp(timestamp) + ); + }), + ]); + + // Rollover Datastream to reset the limit to default which is 1000 + await PageObjects.datasetQuality.rolloverDataStream(degradedDatasetWithLimitDataStreamName); + + // Ingest docs with 26 fields again + await synthtrace.index([ + timerange(moment(to).subtract(count, 'minute'), moment(to)) + .interval('1m') + .rate(1) + .generator((timestamp) => { + return Array(1) + .fill(0) + .flatMap(() => + log + .create() + .dataset(degradedDatasetWithLimitsName) + .message('a log message') + .logLevel(MORE_THAN_1024_CHARS) + .service(serviceName) + .namespace(defaultNamespace) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': serviceName, + 'trace.id': generateShortId(), + test_field: [MORE_THAN_1024_CHARS, 'hello world'], + 'cloud.region': 'us-east-1', + }) + .timestamp(timestamp) + ); + }), + ]); + }); + + describe('field character limit exceeded', () => { + it('should display cause as "field ignored" when a field is ignored due to field above issue', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'test_field', + }); + + await retry.tryForTime(5000, async () => { + const fieldIgnoredMessageExists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-cause', + 'field character limit exceeded' + ); + expect(fieldIgnoredMessageExists).to.be(true); + }); + + await PageObjects.datasetQuality.closeFlyout(); + }); + + it('should display values when cause is "field ignored"', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'test_field', + }); + + await retry.tryForTime(5000, async () => { + const testFieldValueExists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-values', + MORE_THAN_1024_CHARS + ); + expect(testFieldValueExists).to.be(true); + }); + + await PageObjects.datasetQuality.closeFlyout(); + }); + }); + + describe('field limit exceeded', () => { + it('should display cause as "field limit exceeded" when a field is ignored due to field limit issue', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'cloud', + }); + + await retry.tryForTime(5000, async () => { + const fieldLimitMessageExists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-cause', + 'field limit exceeded' + ); + expect(fieldLimitMessageExists).to.be(true); + }); + + await PageObjects.datasetQuality.closeFlyout(); + }); + + it('should display the limit when the cause is "field limit exceeded"', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'cloud', + }); + + await retry.tryForTime(5000, async () => { + const limitExists = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldValue-mappingLimit', + '25' + ); + expect(limitExists).to.be(true); + }); + + await PageObjects.datasetQuality.closeFlyout(); + }); + + it('should warn users about the issue not present in latest backing index', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'cloud', + }); + + await testSubjects.existOrFail( + PageObjects.datasetQuality.testSubjectSelectors + .datasetQualityDetailsDegradedFieldFlyoutIssueDoesNotExist + ); + }); + }); + + describe('current quality issues', () => { + it('should display issues only from latest backing index when current issues toggle is on', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + }); + + const currentIssuesToggleState = + await PageObjects.datasetQuality.getQualityIssueSwitchState(); + + expect(currentIssuesToggleState).to.be(false); + + const rows = + await PageObjects.datasetQuality.getDatasetQualityDetailsDegradedFieldTableRows(); + + expect(rows.length).to.eql(3); + + await testSubjects.click( + PageObjects.datasetQuality.testSubjectSelectors + .datasetQualityDetailsOverviewDegradedFieldToggleSwitch + ); + + const newCurrentIssuesToggleState = + await PageObjects.datasetQuality.getQualityIssueSwitchState(); + + expect(newCurrentIssuesToggleState).to.be(true); + + const newRows = + await PageObjects.datasetQuality.getDatasetQualityDetailsDegradedFieldTableRows(); + + expect(newRows.length).to.eql(2); + }); + + it('should keep the toggle on when url state says so', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'test_field', + showCurrentQualityIssues: true, + }); + + const currentIssuesToggleState = + await PageObjects.datasetQuality.getQualityIssueSwitchState(); + + expect(currentIssuesToggleState).to.be(true); + }); + + it('should display count from latest backing index when current issues toggle is on in the table and in the flyout', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'test_field', + showCurrentQualityIssues: true, + }); + + // Check value in Table + const table = await PageObjects.datasetQuality.parseDegradedFieldTable(); + const countColumn = table['Docs count']; + expect(await countColumn.getCellTexts()).to.eql(['5', '5']); + + // Check value in Flyout + await retry.tryForTime(5000, async () => { + const countValue = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldsList-docCount', + '5' + ); + expect(countValue).to.be(true); + }); + + // Toggle the switch + await testSubjects.click( + PageObjects.datasetQuality.testSubjectSelectors + .datasetQualityDetailsOverviewDegradedFieldToggleSwitch + ); + + // Check value in Table + const newTable = await PageObjects.datasetQuality.parseDegradedFieldTable(); + const newCountColumn = newTable['Docs count']; + expect(await newCountColumn.getCellTexts()).to.eql(['15', '15', '5']); + + // Check value in Flyout + await retry.tryForTime(5000, async () => { + const newCountValue = await PageObjects.datasetQuality.doesTextExist( + 'datasetQualityDetailsDegradedFieldFlyoutFieldsList-docCount', + '15' + ); + expect(newCountValue).to.be(true); + }); + }); + + it('should close the flyout if passed value in URL no more exists in latest backing index and current quality toggle is switched on', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'cloud', + showCurrentQualityIssues: true, + }); + + await testSubjects.missingOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + }); + + it('should close the flyout when current quality switch is toggled on and the flyout is already open with an old field ', async () => { + await PageObjects.datasetQuality.navigateToDetails({ + dataStream: degradedDatasetWithLimitDataStreamName, + expandedDegradedField: 'cloud', + }); + + await testSubjects.existOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + + await testSubjects.click( + PageObjects.datasetQuality.testSubjectSelectors + .datasetQualityDetailsOverviewDegradedFieldToggleSwitch + ); + + await testSubjects.missingOrFail( + PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldFlyout + ); + }); + }); + + after(async () => { + await synthtrace.clean(); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/index.ts b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/index.ts index 699c05de7330f..5a481ac61c757 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/index.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/dataset_quality/index.ts @@ -15,6 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dataset_quality_table_filters')); loadTestFile(require.resolve('./dataset_quality_privileges')); loadTestFile(require.resolve('./dataset_quality_details')); - loadTestFile(require.resolve('./dataset_quality_details_degraded_field_flyout')); + loadTestFile(require.resolve('./degraded_field_flyout')); }); } diff --git a/yarn.lock b/yarn.lock index d4e228d9b10d4..e4992e31488bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7312,17 +7312,16 @@ zod "^3.22.3" zod-to-json-schema "^3.22.5" -"@langchain/core@>0.1.0 <0.3.0", "@langchain/core@>=0.2.11 <0.3.0", "@langchain/core@>=0.2.16 <0.3.0", "@langchain/core@>=0.2.20 <0.3.0", "@langchain/core@>=0.2.5 <0.3.0", "@langchain/core@^0.2.18", "@langchain/core@~0.2.11": - version "0.2.18" - resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.2.18.tgz#1ac4f307fa217ab3555c9634147a6c4ad9826092" - integrity sha512-ru542BwNcsnDfjTeDbIkFIchwa54ctHZR+kVrC8U9NPS9/36iM8p8ruprOV7Zccj/oxtLE5UpEhV+9MZhVcFlA== +"@langchain/core@>0.1.0 <0.3.0", "@langchain/core@>=0.2.11 <0.3.0", "@langchain/core@>=0.2.20 <0.3.0", "@langchain/core@>=0.2.5 <0.3.0", "@langchain/core@^0.2.18", "@langchain/core@~0.2.11": + version "0.2.32" + resolved "https://registry.yarnpkg.com/@langchain/core/-/core-0.2.32.tgz#a5dfbc49f8b6c15c8082763b93aeae8f9f4ca1a0" + integrity sha512-S27M+9Qou2qtcLfFGEvANkJ/zHq5XApeQsR6Q4I7C6v9x07eoYr558h6vVy6WQmKcksgbCIJ854ikwp173wBjA== dependencies: ansi-styles "^5.0.0" camelcase "6" decamelize "1.2.0" js-tiktoken "^1.0.12" - langsmith "~0.1.39" - ml-distance "^4.0.0" + langsmith "^0.1.43" mustache "^4.2.0" p-queue "^6.6.2" p-retry "4" @@ -7330,15 +7329,37 @@ zod "^3.22.4" zod-to-json-schema "^3.22.3" -"@langchain/google-genai@^0.0.23": - version "0.0.23" - resolved "https://registry.yarnpkg.com/@langchain/google-genai/-/google-genai-0.0.23.tgz#e73af501bc1df4c7642b531759b82dc3eb7ae459" - integrity sha512-MTSCJEoKsfU1inz0PWvAjITdNFM4s41uvBCwLpcgx3jWJIEisczFD82x86ahYqJlb2fD6tohYSaCH/4tKAdkXA== +"@langchain/google-common@^0.1.1", "@langchain/google-common@~0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@langchain/google-common/-/google-common-0.1.1.tgz#7f8730e3dfb2920487dece5cbe738c13266b5e7b" + integrity sha512-oT/6lBev/Ufkp1dJbOTJ2S7xD9c+w9CqnqKqFOSxuZJbM4G8hzJtt7PDBOGfamIwtQP8dR7ORKXs1sCl+f5Tig== + dependencies: + uuid "^10.0.0" + zod-to-json-schema "^3.22.4" + +"@langchain/google-gauth@~0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@langchain/google-gauth/-/google-gauth-0.1.0.tgz#ea44941afede32a1d14fd18a6c6b500c7c7a53eb" + integrity sha512-0kps1NmaNiSl4n3lRw+7xsyhrEfIxNqBjih0kNYWPjLg55f9I9+QAlz7F1Sz/628HF1WQLFLQcBQA4geGzvenQ== + dependencies: + "@langchain/google-common" "~0.1.0" + google-auth-library "^8.9.0" + +"@langchain/google-genai@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@langchain/google-genai/-/google-genai-0.1.0.tgz#89552873210d72a5834de20fcbef3e6753283344" + integrity sha512-6rIba77zJVMj+048tLfkCBrkFbfAMiT+AfLEsu5s+CFoFmXMiI/dbKeDL4vhUWrJVb9uL4ZZyrnl0nKxyEKYgA== dependencies: "@google/generative-ai" "^0.7.0" - "@langchain/core" ">=0.2.16 <0.3.0" zod-to-json-schema "^3.22.4" +"@langchain/google-vertexai@^0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@langchain/google-vertexai/-/google-vertexai-0.1.0.tgz#e8ac6ae0bbdb6364d579def171ccfc1a26fd0cf2" + integrity sha512-xTi5NvNGSLQl/7OTsj4QTT0DkNbZ7cYDrEB0HqpZOwo6I5dulh/h2payGVQ6hdXj7Yyv78dRc5FdQSbyHui/WQ== + dependencies: + "@langchain/google-gauth" "~0.1.0" + "@langchain/langgraph@0.0.34": version "0.0.34" resolved "https://registry.yarnpkg.com/@langchain/langgraph/-/langgraph-0.0.34.tgz#1504c29ce524d08d6f076c34e0623c6de1f1246c" @@ -7374,25 +7395,25 @@ dependencies: ncp "^2.0.0" -"@launchdarkly/js-sdk-common@2.8.0": - version "2.8.0" - resolved "https://registry.yarnpkg.com/@launchdarkly/js-sdk-common/-/js-sdk-common-2.8.0.tgz#f9a0c29864ba52e5dc19f273164780005be5c8ab" - integrity sha512-XLYhB0A0vec1gwbnVNU+rzMd1JxdtMJXSA76y3ASJ6X0cG0qTRKQdFba4n4RwIs0y1bX3rncFHJMhBjrXtyPhA== +"@launchdarkly/js-sdk-common@2.9.0": + version "2.9.0" + resolved "https://registry.yarnpkg.com/@launchdarkly/js-sdk-common/-/js-sdk-common-2.9.0.tgz#2828eb12e48c28edaa849821ed9b7824e2ce01b9" + integrity sha512-BYYZ4MgYCc2l650gFotqRMW5oqFJna78+ivf/8Jwy6D3XpRW/DHwrsMBFnVv/G3gAjXq6/dW3ZZ6pS85ZK6mvQ== -"@launchdarkly/js-server-sdk-common@2.6.1": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@launchdarkly/js-server-sdk-common/-/js-server-sdk-common-2.6.1.tgz#e676641c83d8dcc0e9afa8620dd5142eae873127" - integrity sha512-PcyglkUi/P1fBT3GqwZ/K9MvLj2e5Rw5XLP/gC2qIIHJxIbni8oy8Zon+6bP890DYgZtqqeX6tKhAw/gwOYkhQ== +"@launchdarkly/js-server-sdk-common@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@launchdarkly/js-server-sdk-common/-/js-server-sdk-common-2.7.0.tgz#c53fb284af6875b991b6c3935612068719ba9839" + integrity sha512-EFfCVtp5VVl4E2sY6rUa2n0tvAiqstFAloJNN4ft5Tocec+6Te3XKivNdRpMxIrQBZg9c5gX9ofa6B0j0l33RA== dependencies: - "@launchdarkly/js-sdk-common" "2.8.0" + "@launchdarkly/js-sdk-common" "2.9.0" semver "7.5.4" -"@launchdarkly/node-server-sdk@^9.5.4": - version "9.5.4" - resolved "https://registry.yarnpkg.com/@launchdarkly/node-server-sdk/-/node-server-sdk-9.5.4.tgz#7d27456b503c72e05a917873289b94058b49e566" - integrity sha512-1cGKlRP9YY4kgoGhw5Lolv1CoGOH5ywBB++4iqy00PTinyIjbX3LeYrlCWFT3gFrVlmTuc2BoXJvo93qFgkGAA== +"@launchdarkly/node-server-sdk@^9.6.0": + version "9.6.0" + resolved "https://registry.yarnpkg.com/@launchdarkly/node-server-sdk/-/node-server-sdk-9.6.0.tgz#eafdf6cb7d7f3239778d89d78ad7e5b6f7f5f08a" + integrity sha512-vYn+ATN7/QIbBfmsNmeFSfV/8XiwQSPAr31wrTEAr26WvWIpy4HUNHKAtbgRdtFg3fo+7bSuI0FLvFsNVmavcg== dependencies: - "@launchdarkly/js-server-sdk-common" "2.6.1" + "@launchdarkly/js-server-sdk-common" "2.7.0" https-proxy-agent "^5.0.1" launchdarkly-eventsource "2.0.3" @@ -11509,16 +11530,16 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== +"@types/uuid@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" + integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== + "@types/uuid@^9.0.0": version "9.0.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2" integrity sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q== -"@types/uuid@^9.0.1": - version "9.0.2" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.2.tgz#ede1d1b1e451548d44919dc226253e32a6952c4b" - integrity sha512-kNnC1GFBLuhImSnV7w4njQkUiJi0ZXUycu1rUaouPqiKlXkh77JKgdRnTAp1x5eBwcIwbtI+3otwzuIDEuDoxQ== - "@types/vinyl-fs@*", "@types/vinyl-fs@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/vinyl-fs/-/vinyl-fs-3.0.2.tgz#cbaef5160ad7695483af0aa1b4fe67f166c18feb" @@ -12760,7 +12781,7 @@ arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -arrify@^2.0.1: +arrify@^2.0.0, arrify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== @@ -18019,6 +18040,11 @@ fast-stream-to-buffer@^1.0.0: dependencies: end-of-stream "^1.4.1" +fast-text-encoding@^1.0.0: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" + integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== + fastest-levenshtein@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" @@ -18691,6 +18717,16 @@ gauge@^3.0.0: strip-ansi "^6.0.1" wide-align "^1.1.2" +gaxios@^5.0.0, gaxios@^5.0.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-5.1.3.tgz#f7fa92da0fe197c846441e5ead2573d4979e9013" + integrity sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA== + dependencies: + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.6.9" + gaxios@^6.0.0, gaxios@^6.1.1: version "6.6.0" resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.6.0.tgz#af8242fff0bbb82a682840d5feaa91b6a1c58be4" @@ -18702,6 +18738,14 @@ gaxios@^6.0.0, gaxios@^6.1.1: node-fetch "^2.6.9" uuid "^9.0.1" +gcp-metadata@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-5.3.0.tgz#6f45eb473d0cb47d15001476b48b663744d25408" + integrity sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w== + dependencies: + gaxios "^5.0.0" + json-bigint "^1.0.0" + gcp-metadata@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.0.tgz#9b0dd2b2445258e7597f2024332d20611cbd6b8c" @@ -19149,6 +19193,21 @@ gonzales-pe@^4.3.0: dependencies: minimist "^1.2.5" +google-auth-library@^8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-8.9.0.tgz#15a271eb2ec35d43b81deb72211bd61b1ef14dd0" + integrity sha512-f7aQCJODJFmYWN6PeNKzgvy9LI2tYmXnzpNDHEjG5sDNPgGb2FXQyTBnXeSH+PAtpKESFD+LmHw3Ox3mN7e1Fg== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^5.0.0" + gcp-metadata "^5.3.0" + gtoken "^6.1.0" + jws "^4.0.0" + lru-cache "^6.0.0" + google-auth-library@^9.10.0: version "9.10.0" resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.10.0.tgz#c9fb940923f7ff2569d61982ee1748578c0bbfd4" @@ -19161,6 +19220,13 @@ google-auth-library@^9.10.0: gtoken "^7.0.0" jws "^4.0.0" +google-p12-pem@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-4.0.1.tgz#82841798253c65b7dc2a4e5fe9df141db670172a" + integrity sha512-WPkN4yGtz05WZ5EhtlxNDWPhC4JIic6G8ePitwUWy4l+XPVYec+a0j0Ts47PDtW59y3RwAhUd9/h9ZZ63px6RQ== + dependencies: + node-forge "^1.3.1" + google-protobuf@^3.6.1: version "3.19.4" resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.4.tgz#8d32c3e34be9250956f28c0fb90955d13f311888" @@ -19226,6 +19292,15 @@ graphql@^16.6.0, graphql@^16.8.1: resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== +gtoken@^6.1.0: + version "6.1.2" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-6.1.2.tgz#aeb7bdb019ff4c3ba3ac100bbe7b6e74dce0e8bc" + integrity sha512-4ccGpzz7YAr7lxrT2neugmXQ3hP9ho2gcaityLVkiUecAiwiy60Ii8gRbZeOsXV19fYaRjgBSshs8kXw+NKCPQ== + dependencies: + gaxios "^5.0.1" + google-p12-pem "^4.0.0" + jws "^4.0.0" + gtoken@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-7.1.0.tgz#d61b4ebd10132222817f7222b1e6064bd463fc26" @@ -19915,7 +19990,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^5.0.1: +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -22093,16 +22168,17 @@ langchainhub@~0.0.8: resolved "https://registry.yarnpkg.com/langchainhub/-/langchainhub-0.0.8.tgz#fd4b96dc795e22e36c1a20bad31b61b0c33d3110" integrity sha512-Woyb8YDHgqqTOZvWIbm2CaFDGfZ4NTSyXV687AG4vXEfoNo7cGQp7nhl7wL3ehenKWmNEmcxCLgOZzW8jE6lOQ== -langsmith@^0.1.39, langsmith@~0.1.30, langsmith@~0.1.39: - version "0.1.39" - resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.1.39.tgz#cc99f1828a9c0f5ba24bec6b0121edc44e8d282d" - integrity sha512-K2/qbc96JhrZbSL74RbZ0DBOpTB9Mxicu8RQrZ88Xsp1bH2O3+y5EdcvC0g/1YzQWQhcQ4peknCA24c3VTNiNA== +langsmith@^0.1.43, langsmith@^0.1.55, langsmith@~0.1.30: + version "0.1.55" + resolved "https://registry.yarnpkg.com/langsmith/-/langsmith-0.1.55.tgz#bdbb8015a28093f4a248c0ee9b8937731c5baa93" + integrity sha512-6NVtI04UUnIY59I/imOX02FG/QMGfqStu8tiJtyyreKMv2GAN0EE9Z5Ap1wzOe6v8ukEcV3NwEO2LYOPwup1PQ== dependencies: - "@types/uuid" "^9.0.1" + "@types/uuid" "^10.0.0" commander "^10.0.1" p-queue "^6.6.2" p-retry "4" - uuid "^9.0.0" + semver "^7.6.3" + uuid "^10.0.0" language-subtag-registry@~0.3.2: version "0.3.21"