diff --git a/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml b/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml index 467df501bc9ca..1217853275914 100644 --- a/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml +++ b/.buildkite/pipelines/quality-gates/pipeline.kibana-tests.yaml @@ -10,6 +10,11 @@ # # Docs: https://docs.elastic.dev/serverless/qualitygates +agents: + cpu: 2 + ephemeralStorage: "20G" + memory: "8G" + env: TEAM_CHANNEL: "#kibana-mission-control" ENVIRONMENT: ${ENVIRONMENT?} diff --git a/examples/README.asciidoc b/examples/README.asciidoc index d33c5e825ce12..3f2c58a173304 100644 --- a/examples/README.asciidoc +++ b/examples/README.asciidoc @@ -1,7 +1,7 @@ [[example-plugins]] == Example plugins -This folder contains example plugins. To run the plugins in this folder, use the `--run-examples` flag, via +This folder contains example plugins. To run the plugins in this folder, use the `--run-examples` flag (without a basepath), via [source,bash] ---- diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx index cc318831555af..52d4f38b45408 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/configure_pipeline.tsx @@ -19,6 +19,7 @@ import { EuiTabbedContentTab, EuiTitle, EuiText, + EuiTextColor, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -130,15 +131,27 @@ export const ConfigurePipeline: React.FC = () => { )} - - - + + +
+ {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.titleSelectTrainedModel', + { defaultMessage: 'Select a trained ML Model' } + )} +
+
+ {formErrors.modelStatus !== undefined && ( + <> + + +

+ {formErrors.modelStatus} +

+
+ + )} + + ), diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts index a725371de0242..7412d861d7136 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/ml_inference_logic.test.ts @@ -550,6 +550,25 @@ describe('MlInferenceLogic', () => { pipelineName: 'Name already used by another pipeline.', }); }); + it('has errors when non-deployed model is selected', () => { + MLInferenceLogic.actions.setInferencePipelineConfiguration({ + ...MLInferenceLogic.values.addInferencePipelineModal.configuration, + pipelineName: 'unit-test-pipeline', + modelID: 'unit-test-model', + existingPipeline: false, + fieldMappings: [ + { + sourceField: 'body', + targetField: 'ml.inference.body', + }, + ], + isModelPlaceholderSelected: true, + }); + + expect(MLInferenceLogic.values.formErrors).toEqual({ + modelStatus: 'Model must be deployed before use.', + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx index 15fb492fae56d..9bd006f65883e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.test.tsx @@ -98,6 +98,23 @@ describe('ModelSelect', () => { }) ); }); + it('sets placeholder flag on selecting a placeholder item', () => { + setMockValues(DEFAULT_VALUES); + + const wrapper = shallow(); + expect(wrapper.find(EuiSelectable)).toHaveLength(1); + const selectable = wrapper.find(EuiSelectable); + selectable.simulate('change', [ + { modelId: 'model_1' }, + { modelId: 'model_2', isPlaceholder: true, checked: 'on' }, + ]); + expect(MOCK_ACTIONS.setInferencePipelineConfiguration).toHaveBeenCalledWith( + expect.objectContaining({ + modelID: 'model_2', + isModelPlaceholderSelected: true, + }) + ); + }); it('generates pipeline name on selecting an item', () => { setMockValues(DEFAULT_VALUES); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.tsx index 86c91c483702f..ac3900b6ed662 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_select.tsx @@ -44,6 +44,7 @@ export const ModelSelect: React.FC = () => { ...configuration, inferenceConfig: undefined, modelID: selectedOption?.modelId ?? '', + isModelPlaceholderSelected: selectedOption?.isPlaceholder ?? false, fieldMappings: undefined, pipelineName: isPipelineNameUserSupplied ? pipelineName diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/types.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/types.ts index 3a645dcbba3b4..87aed3eb4d714 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/types.ts @@ -13,6 +13,7 @@ export interface InferencePipelineConfiguration { existingPipeline?: boolean; inferenceConfig?: InferencePipelineInferenceConfig; isPipelineNameUserSupplied?: boolean; + isModelPlaceholderSelected?: boolean; modelID: string; pipelineName: string; fieldMappings?: FieldMapping[]; @@ -21,6 +22,7 @@ export interface InferencePipelineConfiguration { export interface AddInferencePipelineFormErrors { modelID?: string; + modelStatus?: string; fieldMappings?: string; pipelineName?: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts index 02cf5a7463dde..5a01a3823a71d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts @@ -37,6 +37,12 @@ const PIPELINE_NAME_EXISTS_ERROR = i18n.translate( defaultMessage: 'Name already used by another pipeline.', } ); +const MODEL_NOT_DEPLOYED_ERROR = i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.addInferencePipelineModal.steps.configure.modelNotDeployedError', + { + defaultMessage: 'Model must be deployed before use.', + } +); export const validateInferencePipelineConfiguration = ( config: InferencePipelineConfiguration @@ -55,6 +61,8 @@ export const validateInferencePipelineConfiguration = ( } if (config.modelID.trim().length === 0) { errors.modelID = FIELD_REQUIRED_ERROR; + } else if (config.isModelPlaceholderSelected) { + errors.modelStatus = MODEL_NOT_DEPLOYED_ERROR; } return errors; diff --git a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts index 22d00451ce231..b58ab3e3fca0c 100644 --- a/x-pack/plugins/fleet/server/services/epm/agent/agent.ts +++ b/x-pack/plugins/fleet/server/services/epm/agent/agent.ts @@ -21,7 +21,6 @@ export function compileTemplate(variables: PackagePolicyConfigRecord, templateSt const { vars, yamlValues } = buildTemplateVariables(logger, variables); let compiledTemplate: string; try { - logger.debug(`Compiling agent template: ${templateStr}`); const template = handlebars.compile(templateStr, { noEscape: true }); compiledTemplate = template(vars); } catch (err) { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 5039891eef59d..8f45e8eee6b13 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -381,7 +381,7 @@ export async function _installPackage({ await packagePolicyService.upgrade(savedObjectsClient, esClient, policyIdsToUpgrade.items); }); } - logger.debug(`Package install - Installation complete}`); + logger.debug(`Package install - Installation complete`); return [...installedKibanaAssetsRefs, ...esReferences]; } catch (err) { if (SavedObjectsErrorHelpers.isConflictError(err)) { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index d4f9cd249177d..33f1b3a6ab73e 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -516,9 +516,7 @@ async function installPackageCommon(options: { } = options; let { telemetryEvent } = options; const logger = appContextService.getLogger(); - logger.info( - `Install - Starting installation of ${pkgName}@${pkgVersion} from ${installSource}, paths: ${paths}` - ); + logger.info(`Install - Starting installation of ${pkgName}@${pkgVersion} from ${installSource} `); // Workaround apm issue with async spans: https://github.com/elastic/apm-agent-nodejs/issues/2611 await Promise.resolve(); diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx index bf7df6091177d..b81f632c9a62c 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/links_menu.tsx @@ -34,6 +34,8 @@ import { formatHumanReadableDateTimeSeconds, timeFormatter } from '@kbn/ml-date- import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils'; import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { CATEGORIZE_FIELD_TRIGGER } from '@kbn/ml-ui-actions'; +import { isDefined } from '@kbn/ml-is-defined'; +import { escapeQuotes } from '@kbn/es-query'; import { PLUGIN_ID } from '../../../../common/constants/app'; import { mlJobService } from '../../services/job_service'; import { findMessageField, getDataViewIdFromName } from '../../util/index_utils'; @@ -265,12 +267,19 @@ export const LinksMenuUI = (props: LinksMenuProps) => { if (record.influencers) { kqlQuery = record.influencers - .map( - (influencer) => - `"${influencer.influencer_field_name}":"${ - influencer.influencer_field_values[0] ?? '' - }"` - ) + .filter((influencer) => isDefined(influencer)) + .map((influencer) => { + const values = influencer.influencer_field_values; + + if (values.length > 0) { + const fieldName = escapeQuotes(influencer.influencer_field_name); + const escapedVals = values + .filter((value) => isDefined(value)) + .map((value) => `"${fieldName}":"${escapeQuotes(value)}"`); + // Ensure there's enclosing () if there are multiple field values, + return escapedVals.length > 1 ? `(${escapedVals.join(' OR ')})` : escapedVals[0]; + } + }) .join(' AND '); } diff --git a/x-pack/plugins/observability/common/locators/paths.ts b/x-pack/plugins/observability/common/locators/paths.ts index dcd3c361c0e36..5db834c867b9c 100644 --- a/x-pack/plugins/observability/common/locators/paths.ts +++ b/x-pack/plugins/observability/common/locators/paths.ts @@ -20,6 +20,7 @@ export const SLOS_WELCOME_PATH = '/slos/welcome' as const; export const SLO_DETAIL_PATH = '/slos/:sloId' as const; export const SLO_CREATE_PATH = '/slos/create' as const; export const SLO_EDIT_PATH = '/slos/edit/:sloId' as const; +export const SLOS_OUTDATED_DEFINITIONS_PATH = '/slos/outdated-definitions' as const; export const CASES_PATH = '/cases' as const; export const paths = { @@ -31,6 +32,7 @@ export const paths = { ruleDetails: (ruleId: string) => `${OBSERVABILITY_BASE_PATH}${RULES_PATH}/${encodeURI(ruleId)}`, slos: `${OBSERVABILITY_BASE_PATH}${SLOS_PATH}`, slosWelcome: `${OBSERVABILITY_BASE_PATH}${SLOS_WELCOME_PATH}`, + slosOutdatedDefinitions: `${OBSERVABILITY_BASE_PATH}${SLOS_OUTDATED_DEFINITIONS_PATH}`, sloCreate: `${OBSERVABILITY_BASE_PATH}${SLO_CREATE_PATH}`, sloCreateWithEncodedForm: (encodedParams: string) => `${OBSERVABILITY_BASE_PATH}${SLO_CREATE_PATH}?_a=${encodedParams}`, diff --git a/x-pack/plugins/observability/public/components/slo/delete_confirmation_modal/slo_delete_confirmation_modal.tsx b/x-pack/plugins/observability/public/components/slo/delete_confirmation_modal/slo_delete_confirmation_modal.tsx index 0822758859d60..eee88b8daf9cb 100644 --- a/x-pack/plugins/observability/public/components/slo/delete_confirmation_modal/slo_delete_confirmation_modal.tsx +++ b/x-pack/plugins/observability/public/components/slo/delete_confirmation_modal/slo_delete_confirmation_modal.tsx @@ -7,11 +7,11 @@ import { EuiConfirmModal } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { ALL_VALUE, SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import React from 'react'; export interface SloDeleteConfirmationModalProps { - slo: SLOWithSummaryResponse; + slo: SLOWithSummaryResponse | SLOResponse; onCancel: () => void; onConfirm: () => void; } diff --git a/x-pack/plugins/observability/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx b/x-pack/plugins/observability/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx new file mode 100644 index 0000000000000..9fabcb61efc96 --- /dev/null +++ b/x-pack/plugins/observability/public/components/slo/reset_confirmation_modal/slo_reset_confirmation_modal.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiConfirmModal } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import React from 'react'; + +export interface SloResetConfirmationModalProps { + slo: SLOWithSummaryResponse | SLOResponse; + onCancel: () => void; + onConfirm: () => void; +} + +export function SloResetConfirmationModal({ + slo, + onCancel, + onConfirm, +}: SloResetConfirmationModalProps) { + const { name } = slo; + return ( + + {i18n.translate('xpack.observability.slo.resetConfirmationModal.descriptionText', { + defaultMessage: 'Resetting this SLO will also regenerate the historical data.', + })} + + ); +} diff --git a/x-pack/plugins/observability/public/components/slo/slo_outdated_callout/index.tsx b/x-pack/plugins/observability/public/components/slo/slo_outdated_callout/index.tsx new file mode 100644 index 0000000000000..dae8f76fb85a5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/slo/slo_outdated_callout/index.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiButton, EuiCallOut } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useFetchSloDefinitions } from '../../../hooks/slo/use_fetch_slo_definitions'; +import { useKibana } from '../../../utils/kibana_react'; +import { paths } from '../../../../common/locators/paths'; + +export function SloOutdatedCallout() { + const { + application: { navigateToUrl }, + http: { basePath }, + } = useKibana().services; + + const handleClick = () => { + navigateToUrl(basePath.prepend(paths.observability.slosOutdatedDefinitions)); + }; + + const { isLoading, data } = useFetchSloDefinitions({ includeOutdatedOnly: true }); + if (!isLoading && data && data.total > 0) { + return ( + +

+ +

+

+ + + +

+
+ ); + } + return null; +} diff --git a/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts b/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts index b56a0576396f9..96d1b55a7cd31 100644 --- a/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts +++ b/x-pack/plugins/observability/public/hooks/slo/query_key_factory.ts @@ -28,7 +28,8 @@ export const sloKeys = { historicalSummaries: () => [...sloKeys.all, 'historicalSummary'] as const, historicalSummary: (list: Array<{ sloId: string; instanceId: string }>) => [...sloKeys.historicalSummaries(), list] as const, - definitions: (search: string) => [...sloKeys.all, 'definitions', search] as const, + definitions: (search: string, page: number, perPage: number, includeOutdatedOnly: boolean) => + [...sloKeys.all, 'definitions', search, page, perPage, includeOutdatedOnly] as const, globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const, burnRates: (sloId: string, instanceId: string | undefined) => [...sloKeys.all, 'burnRates', sloId, instanceId] as const, diff --git a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts index b3b7f59dd37cf..6b6893afb1189 100644 --- a/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts +++ b/x-pack/plugins/observability/public/hooks/slo/use_fetch_slo_definitions.ts @@ -15,23 +15,32 @@ export interface UseFetchSloDefinitionsResponse { isLoading: boolean; isSuccess: boolean; isError: boolean; + refetch: () => void; } interface Params { name?: string; + includeOutdatedOnly?: boolean; + page?: number; + perPage?: number; } -export function useFetchSloDefinitions({ name = '' }: Params): UseFetchSloDefinitionsResponse { +export function useFetchSloDefinitions({ + name = '', + includeOutdatedOnly = false, + page = 1, + perPage = 100, +}: Params): UseFetchSloDefinitionsResponse { const { http } = useKibana().services; const search = name.endsWith('*') ? name : `${name}*`; - const { isLoading, isError, isSuccess, data } = useQuery({ - queryKey: sloKeys.definitions(search), + const { isLoading, isError, isSuccess, data, refetch } = useQuery({ + queryKey: sloKeys.definitions(search, page, perPage, includeOutdatedOnly), queryFn: async ({ signal }) => { try { const response = await http.get( '/api/observability/slos/_definitions', - { query: { search }, signal } + { query: { search, includeOutdatedOnly, page, perPage }, signal } ); return response; @@ -43,5 +52,5 @@ export function useFetchSloDefinitions({ name = '' }: Params): UseFetchSloDefini refetchOnWindowFocus: false, }); - return { isLoading, isError, isSuccess, data }; + return { isLoading, isError, isSuccess, data, refetch }; } diff --git a/x-pack/plugins/observability/public/hooks/slo/use_reset_slo.ts b/x-pack/plugins/observability/public/hooks/slo/use_reset_slo.ts new file mode 100644 index 0000000000000..35f166e89b766 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/slo/use_reset_slo.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 { useMutation } from '@tanstack/react-query'; +import { i18n } from '@kbn/i18n'; +import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; +import { useKibana } from '../../utils/kibana_react'; + +type ServerError = IHttpFetchError; + +export function useResetSlo() { + const { + http, + notifications: { toasts }, + } = useKibana().services; + return useMutation( + ['resetSlo'], + ({ id, name }) => { + try { + return http.post(`/api/observability/slos/${id}/_reset`); + } catch (error) { + return Promise.reject( + i18n.translate('xpack.observability.slo.slo.reset.errorMessage', { + defaultMessage: 'Failed to reset {name} (id: {id}), something went wrong: {error}', + values: { error: String(error), name, id }, + }) + ); + } + }, + { + onError: (error, { name, id }) => { + toasts.addError(new Error(error.body?.message ?? error.message), { + title: i18n.translate('xpack.observability.slo.slo.reset.errorNotification', { + defaultMessage: 'Failed to reset {name} (id: {id})', + values: { name, id }, + }), + }); + }, + onSuccess: (_data, { name }) => { + toasts.addSuccess( + i18n.translate('xpack.observability.slo.slo.reset.successNotification', { + defaultMessage: '{name} reset successfully', + values: { name }, + }) + ); + }, + } + ); +} diff --git a/x-pack/plugins/observability/public/pages/slo_outdated_definitions/index.tsx b/x-pack/plugins/observability/public/pages/slo_outdated_definitions/index.tsx new file mode 100644 index 0000000000000..a16e1a604956b --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slo_outdated_definitions/index.tsx @@ -0,0 +1,141 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTablePagination, EuiText } from '@elastic/eui'; +import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; +import { useKibana } from '../../utils/kibana_react'; +import { useLicense } from '../../hooks/use_license'; +import { usePluginContext } from '../../hooks/use_plugin_context'; +import { useCapabilities } from '../../hooks/slo/use_capabilities'; +import { useFetchSloGlobalDiagnosis } from '../../hooks/slo/use_fetch_global_diagnosis'; +import { HeaderMenu } from '../overview/components/header_menu/header_menu'; +import { useFetchSloDefinitions } from '../../hooks/slo/use_fetch_slo_definitions'; +import { paths } from '../../../common/locators/paths'; +import { SloListEmpty } from '../slos/components/slo_list_empty'; +import { OutdatedSlo } from './outdated_slo'; +import { OutdatedSloSearchBar } from './outdated_slo_search_bar'; + +export function SlosOutdatedDefinitions() { + const { + http: { basePath }, + } = useKibana().services; + const { hasWriteCapabilities } = useCapabilities(); + const { data: globalDiagnosis } = useFetchSloGlobalDiagnosis(); + const { ObservabilityPageTemplate } = usePluginContext(); + + useBreadcrumbs([ + { + href: basePath.prepend(paths.observability.slos), + text: i18n.translate('xpack.observability.breadcrumbs.slosLinkText', { + defaultMessage: 'SLOs', + }), + deepLinkId: 'observability-overview:slos', + }, + { + text: i18n.translate('xpack.observability.breadcrumbs.slosOutdatedDefinitions', { + defaultMessage: 'Outdated SLO Definitions', + }), + }, + ]); + + const [search, setSearch] = useState(''); + const [activePage, setActivePage] = useState(0); + const [perPage, setPerPage] = useState(10); + + const handlePerPageChange = (perPageNumber: number) => { + setPerPage(perPageNumber); + setActivePage(0); + }; + + const { hasAtLeast } = useLicense(); + + const { isLoading, data, refetch } = useFetchSloDefinitions({ + name: search, + includeOutdatedOnly: true, + page: activePage + 1, + perPage, + }); + const { total } = data ?? { total: 0 }; + + const hasRequiredWritePrivileges = + !!globalDiagnosis?.userPrivileges.write.has_all_requested && hasWriteCapabilities; + + const hasPlatinumLicense = hasAtLeast('platinum') === true; + + const hasSlosAndHasPermissions = hasPlatinumLicense && hasRequiredWritePrivileges; + + const errors = !hasRequiredWritePrivileges ? ( + + {i18n.translate('xpack.observability.slo.slosOutdatedDefinitions.sloPermissionsError', { + defaultMessage: 'You must have write permissions for SLOs to access this page', + })} + + ) : !hasPlatinumLicense ? ( + + {i18n.translate('xpack.observability.slo.slosOutdatedDefinitions.licenseError', { + defaultMessage: 'You must have atleast a platinum license to access this page', + })} + + ) : null; + + return ( + + + + {!hasSlosAndHasPermissions ? ( + errors + ) : ( + <> +

+ {i18n.translate('xpack.observability.slo.slosOutdatedDefinitions.description', { + defaultMessage: + 'The following SLOs are from a previous version and need to either be reset to upgrade to the latest version OR deleted and removed from the system. When you reset the SLO, the transform will be updated to the latest version and the historical data will be regenerated from the source data.', + })} +

+ + + + + + {!isLoading && total === 0 && } + {!isLoading && + total > 0 && + data && + data.results.map((slo) => ( + + ))} + + + {!isLoading && data && ( + + )} + + )} +
+ ); +} diff --git a/x-pack/plugins/observability/public/pages/slo_outdated_definitions/outdated_slo.tsx b/x-pack/plugins/observability/public/pages/slo_outdated_definitions/outdated_slo.tsx new file mode 100644 index 0000000000000..f8f5cfeb46848 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slo_outdated_definitions/outdated_slo.tsx @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiButton } from '@elastic/eui'; +import { SLOResponse } from '@kbn/slo-schema'; +import { SloDeleteConfirmationModal } from '../../components/slo/delete_confirmation_modal/slo_delete_confirmation_modal'; +import { SloTimeWindowBadge } from '../slos/components/badges/slo_time_window_badge'; +import { SloIndicatorTypeBadge } from '../slos/components/badges/slo_indicator_type_badge'; +import { useDeleteSlo } from '../../hooks/slo/use_delete_slo'; +import { useResetSlo } from '../../hooks/slo/use_reset_slo'; +import { SloResetConfirmationModal } from '../../components/slo/reset_confirmation_modal/slo_reset_confirmation_modal'; + +interface OutdatedSloProps { + slo: SLOResponse; + onReset: () => void; + onDelete: () => void; +} + +export function OutdatedSlo({ slo, onReset, onDelete }: OutdatedSloProps) { + const { mutateAsync: resetSlo, isLoading: isResetLoading } = useResetSlo(); + const { mutateAsync: deleteSlo, isLoading: isDeleteLoading } = useDeleteSlo(); + const [isDeleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); + const [isResetConfirmationModalOpen, setResetConfirmationModalOpen] = useState(false); + + const handleDelete = () => { + setDeleteConfirmationModalOpen(true); + }; + + const handleDeleteConfirm = async () => { + setDeleteConfirmationModalOpen(false); + await deleteSlo({ id: slo.id, name: slo.name }); + onDelete(); + }; + + const handleDeleteCancel = () => { + setDeleteConfirmationModalOpen(false); + }; + + const handleReset = () => { + setResetConfirmationModalOpen(true); + }; + + const handleResetConfirm = async () => { + setResetConfirmationModalOpen(false); + await resetSlo({ id: slo.id, name: slo.name }); + onReset(); + }; + + const handleResetCancel = () => { + setResetConfirmationModalOpen(false); + }; + return ( + + + + + + + {slo.name} + + + + + + + + + + + + + + + + + + + + {isDeleteConfirmationModalOpen ? ( + + ) : null} + {isResetConfirmationModalOpen ? ( + + ) : null} + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slo_outdated_definitions/outdated_slo_search_bar.tsx b/x-pack/plugins/observability/public/pages/slo_outdated_definitions/outdated_slo_search_bar.tsx new file mode 100644 index 0000000000000..7e491f4933e20 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/slo_outdated_definitions/outdated_slo_search_bar.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiButton, EuiFieldSearch } from '@elastic/eui'; + +interface OutdatedSloSearchBarProps { + initialSearch?: string; + onRefresh: () => void; + onSearch: (search: string) => void; +} + +export function OutdatedSloSearchBar({ + onSearch, + onRefresh, + initialSearch = '', +}: OutdatedSloSearchBarProps) { + const [tempSearch, setTempSearch] = useState(initialSearch); + const [search, setSearch] = useState(initialSearch); + + const refreshOrUpdateSearch = () => { + if (tempSearch !== search) { + setSearch(tempSearch); + onSearch(tempSearch); + } else { + onRefresh(); + } + }; + + const handleClick = (event: React.ChangeEvent) => + setTempSearch(event.target.value); + + const handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + refreshOrUpdateSearch(); + } + }; + + return ( + + + + + + {search === tempSearch && ( + + {i18n.translate('xpack.observability.slosOutdatedDefinitions.refreshButtonLabel', { + defaultMessage: 'Refresh', + })} + + )} + {search !== tempSearch && ( + + {i18n.translate('xpack.observability.slosOutdatedDefinitions.updateButtonLabel', { + defaultMessage: 'Update', + })} + + )} + + + ); +} diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx index c85eb6776680b..316298cd8d44c 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_indicator_type_badge.tsx @@ -7,10 +7,9 @@ import React from 'react'; import { EuiBadge, EuiFlexItem, EuiToolTip, EuiBadgeProps } from '@elastic/eui'; -import { SLOWithSummaryResponse } from '@kbn/slo-schema'; -import { i18n } from '@kbn/i18n'; - +import { SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { euiLightVars } from '@kbn/ui-theme'; +import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../utils/kibana_react'; import { convertSliApmParamsToApmAppDeeplinkUrl } from '../../../../utils/slo/convert_sli_apm_params_to_apm_app_deeplink_url'; import { isApmIndicatorType } from '../../../../utils/slo/indicator'; @@ -18,7 +17,7 @@ import { toIndicatorTypeLabel } from '../../../../utils/slo/labels'; export interface Props { color?: EuiBadgeProps['color']; - slo: SLOWithSummaryResponse; + slo: SLOWithSummaryResponse | SLOResponse; } export function SloIndicatorTypeBadge({ slo, color }: Props) { diff --git a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx index b5d4ecd0224fe..384c17f251b01 100644 --- a/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx +++ b/x-pack/plugins/observability/public/pages/slos/components/badges/slo_time_window_badge.tsx @@ -7,7 +7,7 @@ import { EuiBadge, EuiBadgeProps, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { rollingTimeWindowTypeSchema, SLOWithSummaryResponse } from '@kbn/slo-schema'; +import { rollingTimeWindowTypeSchema, SLOResponse, SLOWithSummaryResponse } from '@kbn/slo-schema'; import { euiLightVars } from '@kbn/ui-theme'; import moment from 'moment'; import React from 'react'; @@ -16,7 +16,7 @@ import { toDurationLabel } from '../../../../utils/slo/labels'; export interface Props { color?: EuiBadgeProps['color']; - slo: SLOWithSummaryResponse; + slo: SLOWithSummaryResponse | SLOResponse; } export function SloTimeWindowBadge({ slo, color }: Props) { diff --git a/x-pack/plugins/observability/public/pages/slos/slos.tsx b/x-pack/plugins/observability/public/pages/slos/slos.tsx index e010df224ce1c..4ea3f27fade3c 100644 --- a/x-pack/plugins/observability/public/pages/slos/slos.tsx +++ b/x-pack/plugins/observability/public/pages/slos/slos.tsx @@ -6,7 +6,7 @@ */ import React, { useEffect, useState } from 'react'; -import { EuiButton } from '@elastic/eui'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; @@ -22,6 +22,7 @@ import { FeedbackButton } from '../../components/slo/feedback_button/feedback_bu import { paths } from '../../../common/locators/paths'; import { useAutoRefreshStorage } from '../../components/slo/auto_refresh_button/hooks/use_auto_refresh_storage'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; +import { SloOutdatedCallout } from '../../components/slo/slo_outdated_callout'; export function SlosPage() { const { @@ -90,6 +91,8 @@ export function SlosPage() { data-test-subj="slosPage" > + + ); diff --git a/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.tsx b/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.tsx index 16f4a75974453..a27762d8553f9 100644 --- a/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.tsx +++ b/x-pack/plugins/observability/public/pages/slos_welcome/slos_welcome.tsx @@ -27,6 +27,7 @@ import { paths } from '../../../common/locators/paths'; import illustration from './assets/illustration.svg'; import { useFetchSloGlobalDiagnosis } from '../../hooks/slo/use_fetch_global_diagnosis'; import { HeaderMenu } from '../overview/components/header_menu/header_menu'; +import { SloOutdatedCallout } from '../../components/slo/slo_outdated_callout'; export function SlosWelcomePage() { const { @@ -62,6 +63,7 @@ export function SlosWelcomePage() { return hasSlosAndHasPermissions || isLoading ? null : ( + diff --git a/x-pack/plugins/observability/public/routes/routes.tsx b/x-pack/plugins/observability/public/routes/routes.tsx index 87fc22c094344..7bfd2c2b25e08 100644 --- a/x-pack/plugins/observability/public/routes/routes.tsx +++ b/x-pack/plugins/observability/public/routes/routes.tsx @@ -31,6 +31,7 @@ import { RULES_LOGS_PATH, RULES_PATH, RULE_DETAIL_PATH, + SLOS_OUTDATED_DEFINITIONS_PATH, SLOS_PATH, SLOS_WELCOME_PATH, SLO_CREATE_PATH, @@ -38,6 +39,7 @@ import { SLO_EDIT_PATH, } from '../../common/locators/paths'; import { HasDataContextProvider } from '../context/has_data_context/has_data_context'; +import { SlosOutdatedDefinitions } from '../pages/slo_outdated_definitions'; // Note: React Router DOM component was not working here // so I've recreated this simple version for this purpose. @@ -158,6 +160,13 @@ export const routes = { params: {}, exact: true, }, + [SLOS_OUTDATED_DEFINITIONS_PATH]: { + handler: () => { + return ; + }, + params: {}, + exact: true, + }, [SLO_EDIT_PATH]: { handler: () => { return ; diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/create_datastream.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/create_datastream.ts index 3ca4572d945ed..491cb5e61122b 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/create_datastream.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/utils/create_datastream.ts @@ -6,8 +6,7 @@ */ // This file is a copy of x-pack/plugins/alerting/server/alerts_service/lib/create_concrete_write_index.ts -// original function create index instead of datastream, and their have plan to use datastream in the future -// so we probably should remove this file and use the original when datastream will be supported +// The original function created an index, while here we create a datastream. If and when responseOps develops first-party code to work with datastreams (https://github.com/elastic/kibana/issues/140403), this file should be removed. import { get } from 'lodash'; import type { IndicesSimulateIndexTemplateResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts index 0e7527bc76887..41a75282e22e9 100644 --- a/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts +++ b/x-pack/test_serverless/functional/test_suites/observability/observability_log_explorer/header_menu.ts @@ -22,7 +22,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'svlCommonNavigation', ]); - describe('Header menu', () => { + // Failing: See https://github.com/elastic/kibana/issues/173165 + // Failing: See https://github.com/elastic/kibana/issues/173165 + describe.skip('Header menu', () => { before(async () => { await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await esArchiver.load(