diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 0356fb4fd65413..a33156e3769721 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -28,7 +28,7 @@ pageLoadAssetSize: dashboardEnhanced: 65646 data: 454087 dataQuality: 19384 - datasetQuality: 50624 + datasetQuality: 52000 dataViewEditor: 28082 dataViewFieldEditor: 42021 dataViewManagement: 5300 diff --git a/x-pack/plugins/data_quality/common/url_schema/common.ts b/x-pack/plugins/data_quality/common/url_schema/common.ts index cf7998f4a1a543..d184fbffe475e7 100644 --- a/x-pack/plugins/data_quality/common/url_schema/common.ts +++ b/x-pack/plugins/data_quality/common/url_schema/common.ts @@ -5,4 +5,50 @@ * 2.0. */ +import * as rt from 'io-ts'; + export const DATA_QUALITY_URL_STATE_KEY = 'pageState'; + +export const directionRT = rt.keyof({ + asc: null, + desc: null, +}); + +export const sortRT = rt.strict({ + field: rt.string, + direction: directionRT, +}); + +export const tableRT = rt.exact( + rt.partial({ + page: rt.number, + rowsPerPage: rt.number, + sort: sortRT, + }) +); + +export const timeRangeRT = rt.strict({ + from: rt.string, + to: rt.string, + refresh: rt.strict({ + pause: rt.boolean, + value: rt.number, + }), +}); + +export const degradedFieldRT = rt.exact( + rt.partial({ + table: tableRT, + }) +); + +export const dataStreamRT = new rt.Type( + 'dataStreamRT', + (input: unknown): input is string => + typeof input === 'string' && (input.match(/-/g) || []).length === 2, + (input, context) => + typeof input === 'string' && (input.match(/-/g) || []).length === 2 + ? rt.success(input) + : rt.failure(input, context), + rt.identity +); diff --git a/x-pack/plugins/data_quality/common/url_schema/dataset_quality_detils_url_schema_v1.ts b/x-pack/plugins/data_quality/common/url_schema/dataset_quality_detils_url_schema_v1.ts new file mode 100644 index 00000000000000..cc92663ff31e9a --- /dev/null +++ b/x-pack/plugins/data_quality/common/url_schema/dataset_quality_detils_url_schema_v1.ts @@ -0,0 +1,25 @@ +/* + * 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 * as rt from 'io-ts'; +import { dataStreamRT, degradedFieldRT, timeRangeRT } from './common'; + +export const urlSchemaRT = rt.exact( + rt.intersection([ + rt.type({ + dataStream: dataStreamRT, + }), + rt.partial({ + v: rt.literal(1), + timeRange: timeRangeRT, + breakdownField: rt.string, + degradedFields: degradedFieldRT, + }), + ]) +); + +export type UrlSchema = rt.TypeOf; diff --git a/x-pack/plugins/data_quality/common/url_schema/url_schema_v1.ts b/x-pack/plugins/data_quality/common/url_schema/dataset_quality_url_schema_v1.ts similarity index 73% rename from x-pack/plugins/data_quality/common/url_schema/url_schema_v1.ts rename to x-pack/plugins/data_quality/common/url_schema/dataset_quality_url_schema_v1.ts index 076e1b641b7e27..78c4faeca8cd84 100644 --- a/x-pack/plugins/data_quality/common/url_schema/url_schema_v1.ts +++ b/x-pack/plugins/data_quality/common/url_schema/dataset_quality_url_schema_v1.ts @@ -6,24 +6,7 @@ */ import * as rt from 'io-ts'; - -export const directionRT = rt.keyof({ - asc: null, - desc: null, -}); - -export const sortRT = rt.strict({ - field: rt.string, - direction: directionRT, -}); - -export const tableRT = rt.exact( - rt.partial({ - page: rt.number, - rowsPerPage: rt.number, - sort: sortRT, - }) -); +import { degradedFieldRT, tableRT, timeRangeRT } from './common'; const integrationRT = rt.strict({ name: rt.string, @@ -46,21 +29,6 @@ const datasetRT = rt.intersection([ ), ]); -const timeRangeRT = rt.strict({ - from: rt.string, - to: rt.string, - refresh: rt.strict({ - pause: rt.boolean, - value: rt.number, - }), -}); - -const degradedFieldRT = rt.exact( - rt.partial({ - table: tableRT, - }) -); - export const flyoutRT = rt.exact( rt.partial({ dataset: datasetRT, diff --git a/x-pack/plugins/data_quality/common/url_schema/index.ts b/x-pack/plugins/data_quality/common/url_schema/index.ts index d3b092e0b0ac83..0af03b6d503f5c 100644 --- a/x-pack/plugins/data_quality/common/url_schema/index.ts +++ b/x-pack/plugins/data_quality/common/url_schema/index.ts @@ -6,4 +6,5 @@ */ export { DATA_QUALITY_URL_STATE_KEY } from './common'; -export * as datasetQualityUrlSchemaV1 from './url_schema_v1'; +export * as datasetQualityUrlSchemaV1 from './dataset_quality_url_schema_v1'; +export * as datasetQualityDetailsUrlSchemaV1 from './dataset_quality_detils_url_schema_v1'; diff --git a/x-pack/plugins/data_quality/public/application.tsx b/x-pack/plugins/data_quality/public/application.tsx index 1c54e1d6003a8b..de4c6ba524a08e 100644 --- a/x-pack/plugins/data_quality/public/application.tsx +++ b/x-pack/plugins/data_quality/public/application.tsx @@ -16,7 +16,7 @@ import { useExecutionContext } from '@kbn/kibana-react-plugin/public'; import { KbnUrlStateStorageFromRouterProvider } from './utils/kbn_url_state_context'; import { useKibanaContextForPluginProvider } from './utils/use_kibana'; import { AppPluginStartDependencies, DataQualityPluginStart } from './types'; -import { DatasetQualityRoute } from './routes'; +import { DatasetQualityRoute, DatasetQualityDetailsRoute } from './routes'; import { PLUGIN_ID } from '../common'; export const renderApp = ( @@ -55,6 +55,7 @@ const AppWithExecutionContext = ({ } /> + } /> diff --git a/x-pack/plugins/data_quality/public/routes/dataset_quality/context.tsx b/x-pack/plugins/data_quality/public/routes/dataset_quality/context.tsx index 8c80ea91daedd2..ddd3227fa1a2b1 100644 --- a/x-pack/plugins/data_quality/public/routes/dataset_quality/context.tsx +++ b/x-pack/plugins/data_quality/public/routes/dataset_quality/context.tsx @@ -7,7 +7,7 @@ import { IToasts } from '@kbn/core-notifications-browser'; import { DatasetQualityPluginStart } from '@kbn/dataset-quality-plugin/public'; -import { DatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller'; +import { DatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality'; import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; import React, { createContext, useContext, useEffect, useState } from 'react'; import { diff --git a/x-pack/plugins/data_quality/public/routes/dataset_quality/index.tsx b/x-pack/plugins/data_quality/public/routes/dataset_quality/index.tsx index 346c72cfdefb38..7ef7c17669e3db 100644 --- a/x-pack/plugins/data_quality/public/routes/dataset_quality/index.tsx +++ b/x-pack/plugins/data_quality/public/routes/dataset_quality/index.tsx @@ -6,7 +6,7 @@ */ import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; -import { DatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller'; +import type { DatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality'; import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { PLUGIN_NAME } from '../../../common'; @@ -21,7 +21,7 @@ export const DatasetQualityRoute = () => { services: { chrome, datasetQuality, notifications, appParams }, } = useKibanaContextForPlugin(); - useBreadcrumbs(PLUGIN_NAME, appParams, chrome); + useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome); return ( ({}); + +interface ContextProps { + children: JSX.Element; + urlStateStorageContainer: IKbnUrlStateStorage; + toastsService: IToasts; + datasetQuality: DatasetQualityPluginStart; +} + +export function DatasetQualityDetailsContextProvider({ + children, + urlStateStorageContainer, + toastsService, + datasetQuality, +}: ContextProps) { + const [controller, setController] = useState(); + const history = useHistory(); + const { + services: { + chrome, + appParams, + application: { navigateToApp }, + }, + } = useKibanaContextForPlugin(); + const rootBreadCrumb = useMemo( + () => ({ + text: PLUGIN_NAME, + onClick: () => navigateToApp('management', { path: `/data/${PLUGIN_ID}` }), + }), + [navigateToApp] + ); + const [breadcrumbs, setBreadcrumbs] = useState([rootBreadCrumb]); + + useEffect(() => { + async function getDatasetQualityDetailsController() { + const initialState = getDatasetQualityDetailsStateFromUrl({ + urlStateStorageContainer, + toastsService, + }); + + // state initialization is under progress + if (initialState === undefined) { + return; + } + + // state initialized but empty + if (initialState === null) { + history.push('/'); + return; + } + + const datasetQualityDetailsController = + await datasetQuality.createDatasetQualityDetailsController({ + initialState, + }); + datasetQualityDetailsController.service.start(); + + setController(datasetQualityDetailsController); + + const datasetQualityStateSubscription = datasetQualityDetailsController.state$.subscribe( + (state) => { + updateUrlFromDatasetQualityDetailsState({ + urlStateStorageContainer, + datasetQualityDetailsState: state, + }); + const breadcrumbValue = getBreadcrumbValue(state.dataStream, state.integration); + setBreadcrumbs([rootBreadCrumb, { text: breadcrumbValue }]); + } + ); + + return () => { + datasetQualityDetailsController.service.stop(); + datasetQualityStateSubscription.unsubscribe(); + }; + } + + getDatasetQualityDetailsController(); + }, [datasetQuality, history, rootBreadCrumb, toastsService, urlStateStorageContainer]); + + useBreadcrumbs(breadcrumbs, appParams, chrome); + + return ( + + {children} + + ); +} + +export const useDatasetQualityDetailsContext = () => { + const context = useContext(DatasetQualityDetailsContext); + if (context === undefined) { + throw new Error( + 'useDatasetQualityDetailContext must be used within a ' + ); + } + return context; +}; diff --git a/x-pack/plugins/data_quality/public/routes/dataset_quality_details/index.tsx b/x-pack/plugins/data_quality/public/routes/dataset_quality_details/index.tsx new file mode 100644 index 00000000000000..956f3795235229 --- /dev/null +++ b/x-pack/plugins/data_quality/public/routes/dataset_quality_details/index.tsx @@ -0,0 +1,65 @@ +/* + * 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 { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; +import type { DatasetQualityDetailsController } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality_details'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKbnUrlStateStorageFromRouterContext } from '../../utils/kbn_url_state_context'; +import { useKibanaContextForPlugin } from '../../utils/use_kibana'; +import { DatasetQualityDetailsContextProvider, useDatasetQualityDetailsContext } from './context'; + +export const DatasetQualityDetailsRoute = () => { + const urlStateStorageContainer = useKbnUrlStateStorageFromRouterContext(); + const { + services: { datasetQuality, notifications }, + } = useKibanaContextForPlugin(); + + return ( + + + + ); +}; + +const ConnectedContent = React.memo(() => { + const { controller } = useDatasetQualityDetailsContext(); + + return controller ? ( + + ) : ( + <> + } + title={ + + } + /> + + ); +}); + +const InitializedContent = React.memo( + ({ + datasetQualityDetailsController, + }: { + datasetQualityDetailsController: DatasetQualityDetailsController; + }) => { + const { + services: { datasetQuality }, + } = useKibanaContextForPlugin(); + + return ; + } +); 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 new file mode 100644 index 00000000000000..b97d1bb9100eb4 --- /dev/null +++ b/x-pack/plugins/data_quality/public/routes/dataset_quality_details/url_schema_v1.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DatasetQualityDetailsPublicStateUpdate } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality_details'; +import * as rt from 'io-ts'; +import { deepCompactObject } from '../../../common/utils/deep_compact_object'; +import { datasetQualityDetailsUrlSchemaV1 } from '../../../common/url_schema'; + +export const getStateFromUrlValue = ( + urlValue: datasetQualityDetailsUrlSchemaV1.UrlSchema +): DatasetQualityDetailsPublicStateUpdate => + deepCompactObject({ + dataStream: urlValue.dataStream, + timeRange: urlValue.timeRange, + degradedFields: urlValue.degradedFields, + }); + +export const getUrlValueFromState = ( + state: DatasetQualityDetailsPublicStateUpdate +): datasetQualityDetailsUrlSchemaV1.UrlSchema => + deepCompactObject({ + dataStream: state.dataStream, + timeRange: state.timeRange, + degradedFields: state.degradedFields, + v: 1, + }); + +const stateFromUrlSchemaRT = new rt.Type< + DatasetQualityDetailsPublicStateUpdate, + datasetQualityDetailsUrlSchemaV1.UrlSchema, + datasetQualityDetailsUrlSchemaV1.UrlSchema +>( + 'stateFromUrlSchemaRT', + rt.never.is, + (urlSchema, _context) => rt.success(getStateFromUrlValue(urlSchema)), + getUrlValueFromState +); + +export const stateFromUntrustedUrlRT = + datasetQualityDetailsUrlSchemaV1.urlSchemaRT.pipe(stateFromUrlSchemaRT); diff --git a/x-pack/plugins/data_quality/public/routes/dataset_quality_details/url_state_storage_service.ts b/x-pack/plugins/data_quality/public/routes/dataset_quality_details/url_state_storage_service.ts new file mode 100644 index 00000000000000..1a71ee6cc33edc --- /dev/null +++ b/x-pack/plugins/data_quality/public/routes/dataset_quality_details/url_state_storage_service.ts @@ -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 { IToasts } from '@kbn/core-notifications-browser'; +import { DatasetQualityDetailsPublicState } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality_details'; +import { createPlainError, formatErrors } from '@kbn/io-ts-utils'; +import { IKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public'; +import * as Either from 'fp-ts/lib/Either'; +import { DatasetQualityDetailsPublicStateUpdate } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality_details'; +import * as rt from 'io-ts'; +import { DATA_QUALITY_URL_STATE_KEY } from '../../../common/url_schema'; +import * as urlSchemaV1 from './url_schema_v1'; + +export const updateUrlFromDatasetQualityDetailsState = ({ + urlStateStorageContainer, + datasetQualityDetailsState, +}: { + urlStateStorageContainer: IKbnUrlStateStorage; + datasetQualityDetailsState?: DatasetQualityDetailsPublicState; +}) => { + if (!datasetQualityDetailsState) { + return; + } + + const encodedUrlStateValues = urlSchemaV1.stateFromUntrustedUrlRT.encode( + datasetQualityDetailsState + ); + + urlStateStorageContainer.set(DATA_QUALITY_URL_STATE_KEY, encodedUrlStateValues, { + replace: true, + }); +}; + +/* + * This function is used to get the dataset quality details state from the URL. + * It will return `null` if the URL state is not present or `undefined` if the URL state is present but invalid. + */ +export const getDatasetQualityDetailsStateFromUrl = ({ + toastsService, + urlStateStorageContainer, +}: { + toastsService: IToasts; + urlStateStorageContainer: IKbnUrlStateStorage; +}): DatasetQualityDetailsPublicStateUpdate | undefined | null => { + const urlStateValues = + urlStateStorageContainer.get(DATA_QUALITY_URL_STATE_KEY) ?? undefined; + + const stateValuesE = rt + .union([rt.undefined, urlSchemaV1.stateFromUntrustedUrlRT]) + .decode(urlStateValues); + + if (Either.isLeft(stateValuesE)) { + withNotifyOnErrors(toastsService).onGetError(createPlainError(formatErrors(stateValuesE.left))); + return undefined; + } else { + return stateValuesE.right ?? null; + } +}; diff --git a/x-pack/plugins/data_quality/public/routes/index.tsx b/x-pack/plugins/data_quality/public/routes/index.tsx index 1a8591d0d3c865..eb8f99aa65ebfb 100644 --- a/x-pack/plugins/data_quality/public/routes/index.tsx +++ b/x-pack/plugins/data_quality/public/routes/index.tsx @@ -6,3 +6,4 @@ */ export * from './dataset_quality'; +export * from './dataset_quality_details'; diff --git a/x-pack/plugins/data_quality/public/utils/use_breadcrumbs.tsx b/x-pack/plugins/data_quality/public/utils/use_breadcrumbs.tsx index 3bf83bcf03352f..b4e6144f3fbac2 100644 --- a/x-pack/plugins/data_quality/public/utils/use_breadcrumbs.tsx +++ b/x-pack/plugins/data_quality/public/utils/use_breadcrumbs.tsx @@ -5,21 +5,31 @@ * 2.0. */ -import type { ChromeStart } from '@kbn/core-chrome-browser'; +import type { ChromeBreadcrumb, ChromeStart } from '@kbn/core-chrome-browser'; -import { ManagementAppMountParams } from '@kbn/management-plugin/public'; import { useEffect } from 'react'; +import { ManagementAppMountParams } from '@kbn/management-plugin/public'; +import { Integration } from '@kbn/dataset-quality-plugin/common/data_streams_stats/integration'; +import { indexNameToDataStreamParts } from '@kbn/dataset-quality-plugin/common'; export const useBreadcrumbs = ( - breadcrumb: string, + breadcrumbs: ChromeBreadcrumb[], params: ManagementAppMountParams, chromeService: ChromeStart ) => { const { docTitle } = chromeService; + const isMultiple = breadcrumbs.length > 1; + + const docTitleValue = isMultiple ? breadcrumbs[breadcrumbs.length - 1].text : breadcrumbs[0].text; - docTitle.change(breadcrumb); + docTitle.change(docTitleValue as string); useEffect(() => { - params.setBreadcrumbs([{ text: breadcrumb }]); - }, [breadcrumb, params]); + params.setBreadcrumbs(breadcrumbs); + }, [breadcrumbs, params]); +}; + +export const getBreadcrumbValue = (dataStream: string, integration?: Integration) => { + const { dataset } = indexNameToDataStreamParts(dataStream); + return integration?.datasets?.[dataset] || dataset; }; 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 a88b2d531b9bc7..f6ba40b8b8bcfa 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 @@ -37,15 +37,15 @@ export const dataStreamStatRt = rt.intersection([ export type DataStreamStat = rt.TypeOf; -export const dashboardRT = rt.type({ +export const integrationDashboardRT = rt.type({ id: rt.string, title: rt.string, }); -export type Dashboard = rt.TypeOf; +export type Dashboard = rt.TypeOf; export const integrationDashboardsRT = rt.type({ - dashboards: rt.array(dashboardRT), + dashboards: rt.array(integrationDashboardRT), }); export type IntegrationDashboardsResponse = rt.TypeOf; @@ -116,6 +116,7 @@ export type DegradedFieldResponse = rt.TypeOf; diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/errors.ts b/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/errors.ts deleted file mode 100644 index eb74f08ab1336c..00000000000000 --- a/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/errors.ts +++ /dev/null @@ -1,18 +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 class GetDataStreamsDetailsError extends Error { - readonly statusCode?: number; - - constructor(message: string, statusCode?: number) { - super(message); - Object.setPrototypeOf(this, new.target.prototype); - this.name = 'GetDataStreamsDetailsError'; - - this.statusCode = statusCode; - } -} diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/index.ts b/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/index.ts index 8761ef1b52a32f..6cc0ccaa93a6d1 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/index.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/data_stream_details/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './errors'; +export * from './types'; diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/errors.ts b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/errors.ts deleted file mode 100644 index aa68ed0b5972f9..00000000000000 --- a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/errors.ts +++ /dev/null @@ -1,17 +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 class GetDataStreamsStatsError extends Error { - readonly statusCode?: number; - - constructor(message: string, statusCode?: number) { - super(message); - Object.setPrototypeOf(this, new.target.prototype); - this.name = 'GetDataStreamsStatsError'; - this.statusCode = statusCode; - } -} diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/index.ts b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/index.ts index 28c7b0a8c274f4..6cc0ccaa93a6d1 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/index.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/index.ts @@ -6,4 +6,3 @@ */ export * from './types'; -export * from './errors'; diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/types.ts b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/types.ts index 9fe23f6ceef6ff..1963c73d263e70 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/data_streams_stats/types.ts @@ -61,8 +61,6 @@ export type GetDataStreamDetailsResponse = export type GetNonAggregatableDataStreamsParams = APIClientRequestParamsOf<`GET /internal/dataset_quality/data_streams/non_aggregatable`>['params']['query']; -export type GetNonAggregatableDataStreamsResponse = - APIReturnType<`GET /internal/dataset_quality/data_streams/non_aggregatable`>; export type GetIntegrationDashboardsParams = APIClientRequestParamsOf<`GET /internal/dataset_quality/integrations/{integration}/dashboards`>['params']['path']; diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/errors.ts b/x-pack/plugins/observability_solution/dataset_quality/common/errors.ts new file mode 100644 index 00000000000000..aecca58aebb5cf --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/common/errors.ts @@ -0,0 +1,25 @@ +/* + * 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 { ApiErrorResponse } from './fetch_options'; + +export class DatasetQualityError extends Error { + readonly statusCode?: number; + readonly originalMessage?: string; + + constructor(message: string, originalError?: ApiErrorResponse) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + this.name = 'DatasetQualityError'; + + if (originalError && originalError.body) { + const { statusCode, message: originalMessage } = originalError.body; + this.statusCode = statusCode; + this.originalMessage = originalMessage; + } + } +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/fetch_options.ts b/x-pack/plugins/observability_solution/dataset_quality/common/fetch_options.ts index 3a72a72762dee9..14da6902ce95c8 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/fetch_options.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/fetch_options.ts @@ -12,3 +12,12 @@ export type FetchOptions = Omit & { method?: string; body?: any; }; + +export interface ApiErrorResponse { + body: { + statusCode: number; + error: string; + message: string; + attributes: object; + }; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/index.ts b/x-pack/plugins/observability_solution/dataset_quality/common/index.ts index b015815eeaacc0..a4cb63ef339bb4 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/index.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/index.ts @@ -8,3 +8,4 @@ export type { DatasetQualityConfig } from './plugin_config'; export type { FetchOptions } from './fetch_options'; export type { APIClientRequestParamsOf, APIReturnType } from './rest'; +export { indexNameToDataStreamParts } from './utils'; 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 6ab0fde0f033d8..959932bd0c5acf 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/translations.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/translations.ts @@ -43,15 +43,15 @@ export const flyoutCancelText = i18n.translate('xpack.datasetQuality.flyoutCance defaultMessage: 'Cancel', }); -export const flyoutOpenInLogsExplorerText = i18n.translate( - 'xpack.datasetQuality.flyoutOpenInLogsExplorerText', +export const openInLogsExplorerText = i18n.translate( + 'xpack.datasetQuality.details.openInLogsExplorerText', { defaultMessage: 'Open in Logs Explorer', } ); -export const flyoutOpenInDiscoverText = i18n.translate( - 'xpack.datasetQuality.flyoutOpenInDiscoverText', +export const openInDiscoverText = i18n.translate( + 'xpack.datasetQuality.details.openInDiscoverText', { defaultMessage: 'Open in Discover', } @@ -64,20 +64,6 @@ export const flyoutDatasetDetailsText = i18n.translate( } ); -export const flyoutDatasetLastActivityText = i18n.translate( - 'xpack.datasetQuality.flyoutDatasetLastActivityText', - { - defaultMessage: 'Last Activity', - } -); - -export const flyoutDatasetCreatedOnText = i18n.translate( - 'xpack.datasetQuality.flyoutDatasetCreatedOnText', - { - defaultMessage: 'Created on', - } -); - export const flyoutIntegrationDetailsText = i18n.translate( 'xpack.datasetQuality.flyoutIntegrationDetailsText', { @@ -85,13 +71,6 @@ export const flyoutIntegrationDetailsText = i18n.translate( } ); -export const flyoutIntegrationVersionText = i18n.translate( - 'xpack.datasetQuality.flyoutIntegrationVersionText', - { - defaultMessage: 'Version', - } -); - export const flyoutIntegrationNameText = i18n.translate( 'xpack.datasetQuality.flyoutIntegrationNameText', { @@ -103,7 +82,7 @@ export const flyoutSummaryText = i18n.translate('xpack.datasetQuality.flyoutSumm defaultMessage: 'Summary', }); -export const flyoutDegradedDocsText = i18n.translate( +export const overviewDegradedDocsText = i18n.translate( 'xpack.datasetQuality.flyout.degradedDocsTitle', { defaultMessage: 'Degraded docs', @@ -147,34 +126,6 @@ export const flyoutHostsText = i18n.translate('xpack.datasetQuality.flyoutHostsT export const flyoutShowAllText = i18n.translate('xpack.datasetQuality.flyoutShowAllText', { defaultMessage: 'Show all', }); - -export const flyoutImprovementText = i18n.translate( - 'xpack.datasetQuality.flyoutDegradedFieldsSectionTitle', - { - defaultMessage: 'Degraded fields', - } -); - -export const flyoutImprovementTooltip = i18n.translate( - 'xpack.datasetQuality.flyoutDegradedFieldsSectionTooltip', - { - defaultMessage: 'A partial list of degraded fields found in your data set.', - } -); - -export const flyoutDegradedFieldsTableLoadingText = i18n.translate( - 'xpack.datasetQuality.flyoutDegradedFieldsTableLoadingText', - { - defaultMessage: 'Loading degraded fields', - } -); - -export const flyoutDegradedFieldsTableNoData = i18n.translate( - 'xpack.datasetQuality.flyoutDegradedFieldsTableNoData', - { - defaultMessage: 'No degraded fields found', - } -); /* Summary Panel */ @@ -271,3 +222,155 @@ export const fullDatasetNameDescription = i18n.translate( defaultMessage: 'Turn on to show the actual data set names used to store the documents.', } ); + +export const flyoutImprovementText = i18n.translate( + 'xpack.datasetQuality.flyoutDegradedFieldsSectionTitle', + { + defaultMessage: 'Degraded fields', + } +); + +export const flyoutImprovementTooltip = i18n.translate( + 'xpack.datasetQuality.flyoutDegradedFieldsSectionTooltip', + { + defaultMessage: 'A partial list of degraded fields found in your data set.', + } +); + +/* +Dataset Quality Details +*/ + +export const overviewHeaderTitle = i18n.translate('xpack.datasetQuality.details.overviewTitle', { + defaultMessage: 'Overview', +}); + +export const overviewTitleTooltip = i18n.translate( + 'xpack.datasetQuality.details.overviewTitleTooltip', + { + defaultMessage: 'Stats of the data set within the selected time range.', + } +); + +export const overviewPanelTitleDocuments = i18n.translate( + 'xpack.datasetQuality.details.overviewPanel.documents.title', + { + defaultMessage: 'Documents', + } +); + +export const overviewPanelDocumentsIndicatorTotalCount = i18n.translate( + 'xpack.datasetQuality.details.overviewPanel.documents.totalCount', + { + defaultMessage: 'Total count', + } +); + +export const overviewPanelDocumentsIndicatorSize = i18n.translate( + 'xpack.datasetQuality.details.overviewPanel.documents.size', + { + defaultMessage: 'Size', + } +); + +export const overviewPanelTitleResources = i18n.translate( + 'xpack.datasetQuality.details.overviewPanel.resources.title', + { + defaultMessage: 'Resources', + } +); + +export const overviewPanelResourcesIndicatorServices = i18n.translate( + 'xpack.datasetQuality.details.overviewPanel.resources.services', + { + defaultMessage: 'Services', + } +); + +export const overviewPanelResourcesIndicatorSize = i18n.translate( + 'xpack.datasetQuality.details.overviewPanel.resources.hosts', + { + defaultMessage: 'Hosts', + } +); + +export const overviewPanelTitleDatasetQuality = i18n.translate( + 'xpack.datasetQuality.details.overviewPanel.datasetQuality.title', + { + defaultMessage: 'Data set quality', + } +); + +export const overviewPanelDatasetQualityIndicatorDegradedDocs = i18n.translate( + 'xpack.datasetQuality.details.overviewPanel.datasetQuality.degradedDocs', + { + defaultMessage: 'Degraded docs', + } +); + +export const overviewDegradedFieldsTableLoadingText = i18n.translate( + 'xpack.datasetQuality.details.degradedFieldsTableLoadingText', + { + defaultMessage: 'Loading degraded fields', + } +); + +export const overviewDegradedFieldsTableNoData = i18n.translate( + 'xpack.datasetQuality.details.degradedFieldsTableNoData', + { + defaultMessage: 'No degraded fields found', + } +); + +export const overviewDegradedFieldsSectionTitle = i18n.translate( + 'xpack.datasetQuality.detail.degradedFieldsSectionTitle', + { + defaultMessage: 'Quality issues', + } +); + +export const overviewDegradedFieldsSectionTitleTooltip = i18n.translate( + 'xpack.datasetQuality.details.degradedFieldsSectionTooltip', + { + defaultMessage: 'A partial list of quality issues found in your data set.', + } +); + +export const overviewQualityIssuesAccordionTechPreviewBadge = i18n.translate( + 'xpack.datasetQuality.details.overviewQualityIssuesAccordionTechPreviewBadge', + { + defaultMessage: 'TECH PREVIEW', + } +); + +export const detailsHeaderTitle = i18n.translate('xpack.datasetQuality.details.detailsTitle', { + defaultMessage: 'Details', +}); + +export const datasetLastActivityText = i18n.translate( + 'xpack.datasetQuality.details.datasetLastActivityText', + { + defaultMessage: 'Last Activity', + } +); + +export const datasetCreatedOnText = i18n.translate( + 'xpack.datasetQuality.details.datasetCreatedOnText', + { + defaultMessage: 'Created on', + } +); + +export const integrationNameText = i18n.translate( + 'xpack.datasetQuality.details.integrationnameText', + { + defaultMessage: 'Integration', + } +); + +export const integrationVersionText = i18n.translate( + 'xpack.datasetQuality.details.integrationVersionText', + { + defaultMessage: 'Version', + } +); diff --git a/x-pack/plugins/observability_solution/dataset_quality/common/types/common.ts b/x-pack/plugins/observability_solution/dataset_quality/common/types/common.ts index 82d7b64e25f636..48d19ac0f4086c 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/common/types/common.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/common/types/common.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { RefreshInterval, TimeRange } from '@kbn/data-plugin/common'; import { DataStreamStatType } from '../data_streams_stats'; import { Integration } from '../data_streams_stats/integration'; @@ -24,3 +25,16 @@ export interface BasicDataStream { namespace: string; integration?: Integration; } + +export interface TableCriteria { + page: number; + rowsPerPage: number; + sort: { + field: TSortField; + direction: SortDirection; + }; +} + +export type TimeRangeConfig = Pick & { + refresh: RefreshInterval; +}; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/dataset_quality.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/dataset_quality.tsx index 8ae6f1f74bd2eb..4c44effcce8bca 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/dataset_quality.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/dataset_quality.tsx @@ -12,7 +12,7 @@ import { PerformanceContextProvider } from '@kbn/ebt-tools'; import { DatasetQualityContext, DatasetQualityContextValue } from './context'; import { useKibanaContextForPluginProvider } from '../../utils'; import { DatasetQualityStartDeps } from '../../types'; -import { DatasetQualityController } from '../../controller'; +import { DatasetQualityController } from '../../controller/dataset_quality'; import { ITelemetryClient } from '../../services/telemetry'; export interface DatasetQualityProps { diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/columns.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/columns.tsx index 515307df1a200e..9a0b1f21ff9f13 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/columns.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/columns.tsx @@ -37,6 +37,7 @@ import { PrivilegesWarningIconWrapper, IntegrationIcon } from '../../common'; import { useRedirectLink } from '../../../hooks'; import { FlyoutDataset } from '../../../state_machines/dataset_quality_controller'; import { DegradedDocsPercentageLink } from './degraded_docs_percentage_link'; +import { TimeRangeConfig } from '../../../../common/types'; const expandDatasetAriaLabel = i18n.translate('xpack.datasetQuality.expandLabel', { defaultMessage: 'Expand', @@ -167,6 +168,7 @@ export const getDatasetQualityTableColumns = ({ showFullDatasetNames, isSizeStatsAvailable, isActiveDataset, + timeRange, }: { fieldFormats: FieldFormatsStart; canUserMonitorDataset: boolean; @@ -178,6 +180,7 @@ export const getDatasetQualityTableColumns = ({ isSizeStatsAvailable: boolean; openFlyout: (selectedDataset: FlyoutDataset) => void; isActiveDataset: (lastActivity: number) => boolean; + timeRange: TimeRangeConfig; }): Array> => { return [ { @@ -296,6 +299,7 @@ export const getDatasetQualityTableColumns = ({ ), width: '140px', @@ -339,7 +343,11 @@ export const getDatasetQualityTableColumns = ({ { name: actionsColumnName, render: (dataStreamStat: DataStreamStat) => ( - + ), width: '100px', }, @@ -349,13 +357,16 @@ export const getDatasetQualityTableColumns = ({ const RedirectLink = ({ dataStreamStat, title, + timeRange, }: { dataStreamStat: DataStreamStat; title: string; + timeRange: TimeRangeConfig; }) => { const redirectLinkProps = useRedirectLink({ dataStreamStat, telemetry: { page: 'main', navigationSource: NavigationSource.Table }, + timeRangeConfig: timeRange, }); return ( diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/degraded_docs_percentage_link.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/degraded_docs_percentage_link.tsx index 9e8fb79168fd41..54c393323ccfd3 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/degraded_docs_percentage_link.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality/table/degraded_docs_percentage_link.tsx @@ -12,13 +12,16 @@ import { NavigationSource } from '../../../services/telemetry'; import { useRedirectLink } from '../../../hooks'; import { QualityPercentageIndicator } from '../../quality_indicator'; import { DataStreamStat } from '../../../../common/data_streams_stats/data_stream_stat'; +import { TimeRangeConfig } from '../../../../common/types'; export const DegradedDocsPercentageLink = ({ isLoading, dataStreamStat, + timeRange, }: { isLoading: boolean; dataStreamStat: DataStreamStat; + timeRange: TimeRangeConfig; }) => { const { degradedDocs: { percentage, count }, @@ -31,6 +34,7 @@ export const DegradedDocsPercentageLink = ({ page: 'main', navigationSource: NavigationSource.Table, }, + timeRangeConfig: timeRange, }); return ( diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/context.ts b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/context.ts new file mode 100644 index 00000000000000..4b54f1fc735794 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/context.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createContext, useContext } from 'react'; +import { DatasetQualityDetailsControllerStateService } from '../../state_machines/dataset_quality_details_controller'; +import { ITelemetryClient } from '../../services/telemetry'; + +export interface DatasetQualityDetailsContextValue { + service: DatasetQualityDetailsControllerStateService; + telemetryClient: ITelemetryClient; +} + +export const DatasetQualityDetailsContext = createContext({} as DatasetQualityDetailsContextValue); + +export function useDatasetQualityDetailsContext() { + return useContext(DatasetQualityDetailsContext); +} 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 new file mode 100644 index 00000000000000..522ad22fae4292 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/dataset_quality_details.tsx @@ -0,0 +1,32 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import { useDatasetQualityDetailsState } from '../../hooks'; +import { DataStreamNotFoundPrompt } from './index_not_found_prompt'; +import { Header } from './header'; +import { Overview } from './overview'; +import { Details } from './details'; + +// Allow for lazy loading +// eslint-disable-next-line import/no-default-export +export default function DatasetQualityDetails() { + const { isIndexNotFoundError, dataStream } = useDatasetQualityDetailsState(); + return isIndexNotFoundError ? ( + + ) : ( + + +
+ + + +
+ + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/dataset_summary.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/dataset_summary.tsx new file mode 100644 index 00000000000000..17032d7866d05c --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/dataset_summary.tsx @@ -0,0 +1,95 @@ +/* + * 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, { Fragment } from 'react'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; +import { EuiBadge, EuiFlexGroup, EuiPanel, EuiText } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { IntegrationActionsMenu } from './integration_actions_menu'; +import { + datasetCreatedOnText, + datasetLastActivityText, + integrationNameText, + integrationVersionText, +} from '../../../../common/translations'; +import { FieldsList } from './fields_list'; +import { useDatasetQualityDetailsState } from '../../../hooks'; +import { IntegrationIcon } from '../../common'; + +export function DatasetSummary() { + const { fieldFormats, dataStreamSettings, dataStreamDetails, loadingState, integrationDetails } = + useDatasetQualityDetailsState(); + const dataFormatter = fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.DATE, [ + ES_FIELD_TYPES.DATE, + ]); + const { + dataStreamDetailsLoading, + dataStreamSettingsLoading, + integrationDetailsLoadings, + integrationDashboardsLoading, + } = loadingState; + const formattedLastActivity = dataStreamDetails?.lastActivity + ? dataFormatter.convert(dataStreamDetails?.lastActivity) + : '-'; + const formattedCreatedOn = dataStreamSettings?.createdOn + ? dataFormatter.convert(dataStreamSettings.createdOn) + : '-'; + + return ( + + + + + + {integrationDetails.integration?.name} + + + ), + actionsMenu: ( + + ), + isLoading: integrationDetailsLoadings, + }, + { + fieldTitle: integrationVersionText, + fieldValue: integrationDetails.integration?.version, + isLoading: integrationDetailsLoadings, + }, + ] + : []), + { + fieldTitle: datasetLastActivityText, + fieldValue: formattedLastActivity, + isLoading: dataStreamDetailsLoading, + }, + { + fieldTitle: datasetCreatedOnText, + fieldValue: formattedCreatedOn, + isLoading: dataStreamSettingsLoading, + }, + ]} + /> + + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/fields_list.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/fields_list.tsx new file mode 100644 index 00000000000000..f48ad1d932357e --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/fields_list.tsx @@ -0,0 +1,54 @@ +/* + * 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, { ReactNode, Fragment } from 'react'; +import { + EuiFlexGroup, + EuiPanel, + EuiFlexItem, + EuiTitle, + EuiHorizontalRule, + EuiSkeletonRectangle, +} from '@elastic/eui'; + +export function FieldsList({ + fields, + dataTestSubj = 'datasetQualityDetailsFieldsList', +}: { + fields: Array<{ + fieldTitle: string; + fieldValue: ReactNode; + isLoading: boolean; + actionsMenu?: ReactNode; + }>; + dataTestSubj?: string; +}) { + return ( + + + {fields.map(({ fieldTitle, fieldValue, isLoading: isFieldLoading, actionsMenu }, index) => ( + + + + + {fieldTitle} + + + + + {fieldValue} + + + {actionsMenu} + + {index < fields.length - 1 ? : null} + + ))} + + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/header.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/header.tsx new file mode 100644 index 00000000000000..a286751818a0ab --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/header.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiTitle } from '@elastic/eui'; +import { detailsHeaderTitle } from '../../../../common/translations'; + +export function DetailsHeader() { + return ( + + {detailsHeaderTitle} + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/index.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/index.tsx new file mode 100644 index 00000000000000..2c68a5fe46d541 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiSpacer } from '@elastic/eui'; +import { DetailsHeader } from './header'; +import { DatasetSummary } from './dataset_summary'; + +export function Details() { + return ( + <> + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/integration_actions_menu.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/integration_actions_menu.tsx new file mode 100644 index 00000000000000..4d9591be1bd58e --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/details/integration_actions_menu.tsx @@ -0,0 +1,206 @@ +/* + * 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, { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiContextMenu, + EuiContextMenuPanelDescriptor, + EuiContextMenuPanelItemDescriptor, + EuiPopover, + EuiSkeletonRectangle, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import type { RouterLinkProps } from '@kbn/router-utils/src/get_router_link_props'; +import { Integration } from '../../../../common/data_streams_stats/integration'; +import { Dashboard } from '../../../../common/api_types'; +import { useDatasetQualityDetailsState, useIntegrationActions } from '../../../hooks'; + +const integrationActionsText = i18n.translate( + 'xpack.datasetQuality.details.integrationActionsText', + { + defaultMessage: 'Integration actions', + } +); + +const seeIntegrationText = i18n.translate('xpack.datasetQuality.details.seeIntegrationActionText', { + defaultMessage: 'See integration', +}); + +const indexTemplateText = i18n.translate('xpack.datasetQuality.details.indexTemplateActionText', { + defaultMessage: 'Index template', +}); + +const viewDashboardsText = i18n.translate('xpack.datasetQuality.details.viewDashboardsActionText', { + defaultMessage: 'View dashboards', +}); + +export function IntegrationActionsMenu({ + integration, + dashboards, + dashboardsLoading, +}: { + integration: Integration; + dashboards?: Dashboard[]; + dashboardsLoading: boolean; +}) { + const { canUserAccessDashboards, canUserViewIntegrations, datasetDetails } = + useDatasetQualityDetailsState(); + const { version, name: integrationName } = integration; + const { type, name } = datasetDetails; + const { + isOpen, + handleCloseMenu, + handleToggleMenu, + getIntegrationOverviewLinkProps, + getIndexManagementLinkProps, + getDashboardLinkProps, + } = useIntegrationActions(); + + const actionButton = ( + + ); + + const MenuActionItem = ({ + dataTestSubject, + buttonText, + routerLinkProps, + iconType, + disabled = false, + }: { + dataTestSubject: string; + buttonText: string | React.ReactNode; + routerLinkProps: RouterLinkProps; + iconType: string; + disabled?: boolean; + }) => ( + + {buttonText} + + ); + + const panelItems = useMemo(() => { + const firstLevelItems: EuiContextMenuPanelItemDescriptor[] = [ + ...(canUserViewIntegrations + ? [ + { + renderItem: () => ( + + ), + }, + ] + : []), + { + renderItem: () => ( + + ), + }, + { + isSeparator: true, + key: 'sep', + }, + ]; + + if (dashboards?.length && canUserAccessDashboards) { + firstLevelItems.push({ + icon: 'dashboardApp', + panel: 1, + name: viewDashboardsText, + 'data-test-subj': 'datasetQualityDetailsIntegrationActionViewDashboards', + disabled: false, + }); + } else if (dashboardsLoading) { + firstLevelItems.push({ + icon: 'dashboardApp', + name: , + 'data-test-subj': 'datasetQualityDetailsIntegrationActionDashboardsLoading', + disabled: true, + }); + } + + const panel: EuiContextMenuPanelDescriptor[] = [ + { + id: 0, + items: firstLevelItems, + }, + { + id: 1, + title: viewDashboardsText, + items: dashboards?.map((dashboard) => { + return { + renderItem: () => ( + + ), + }; + }), + }, + ]; + + return panel; + }, [ + dashboards, + getDashboardLinkProps, + getIndexManagementLinkProps, + getIntegrationOverviewLinkProps, + integrationName, + name, + type, + version, + dashboardsLoading, + canUserAccessDashboards, + canUserViewIntegrations, + ]); + + return ( + + + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/header.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/header.tsx new file mode 100644 index 00000000000000..2a0e3e93e32ac7 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/header.tsx @@ -0,0 +1,92 @@ +/* + * 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, + EuiFlexGroup, + EuiFlexItem, + EuiSkeletonTitle, + EuiTextColor, + EuiTitle, + useEuiShadow, + useEuiTheme, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import React from 'react'; +import { openInDiscoverText, openInLogsExplorerText } from '../../../common/translations'; +import { useDatasetQualityDetailsRedirectLink, useDatasetQualityDetailsState } from '../../hooks'; +import { IntegrationIcon } from '../common'; + +export function Header() { + const { datasetDetails, timeRange, integrationDetails, loadingState } = + useDatasetQualityDetailsState(); + + const { rawName, name: title } = datasetDetails; + const euiShadow = useEuiShadow('s'); + const { euiTheme } = useEuiTheme(); + const redirectLinkProps = useDatasetQualityDetailsRedirectLink({ + dataStreamStat: datasetDetails, + timeRangeConfig: timeRange, + }); + + const pageTitle = integrationDetails?.integration?.datasets?.[datasetDetails.name] ?? title; + + return !loadingState.integrationDetailsLoaded ? ( + + ) : ( + + + + + +

{pageTitle}

+
+
+ +
+
+

+ {rawName} +

+
+
+ + + + {redirectLinkProps.isLogsExplorerAvailable + ? openInLogsExplorerText + : openInDiscoverText} + + + +
+ ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/index.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/index.tsx new file mode 100644 index 00000000000000..dbeb0c874d6665 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/index.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { dynamic } from '@kbn/shared-ux-utility'; +import { PerformanceContextProvider } from '@kbn/ebt-tools'; +import { DatasetQualityDetailsController } from '../../controller/dataset_quality_details'; +import { DatasetQualityStartDeps } from '../../types'; +import { ITelemetryClient } from '../../services/telemetry'; +import { useKibanaContextForPluginProvider } from '../../utils'; + +import { DatasetQualityDetailsContext, DatasetQualityDetailsContextValue } from './context'; + +const DatasetQualityDetails = dynamic(() => import('./dataset_quality_details')); + +export interface DatasetQualityDetailsProps { + controller: DatasetQualityDetailsController; +} + +export interface CreateDatasetQualityArgs { + core: CoreStart; + plugins: DatasetQualityStartDeps; + telemetryClient: ITelemetryClient; +} + +export const createDatasetQualityDetails = ({ + core, + plugins, + telemetryClient, +}: CreateDatasetQualityArgs) => { + return ({ controller }: DatasetQualityDetailsProps) => { + const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider(core, plugins); + + const datasetQualityDetailsProviderValue: DatasetQualityDetailsContextValue = useMemo( + () => ({ + service: controller.service, + telemetryClient, + }), + [controller.service] + ); + + return ( + + + + + + + + ); + }; +}; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/index_not_found_prompt.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/index_not_found_prompt.tsx new file mode 100644 index 00000000000000..72b896a41a8db4 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/index_not_found_prompt.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const emptyPromptTitle = i18n.translate('xpack.datasetQuality.details.emptypromptTitle', { + defaultMessage: 'Unable to load your data stream', +}); + +const emptyPromptBody = (dataStream: string) => + i18n.translate('xpack.datasetQuality.details.emptyPromptBody', { + defaultMessage: 'Data stream not found: {dataStream}', + values: { + dataStream, + }, + }); + +export function DataStreamNotFoundPrompt({ dataStream }: { dataStream: string }) { + const promptTitle =

{emptyPromptTitle}

; + const promptBody =

{emptyPromptBody(dataStream)}

; + + return ; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/aggregation_not_supported.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/aggregation_not_supported.tsx new file mode 100644 index 00000000000000..e9ec36c763454a --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/aggregation_not_supported.tsx @@ -0,0 +1,75 @@ +/* + * 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 { EuiCallOut, EuiCode, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +const aggregationNotSupportedTitle = i18n.translate('xpack.datasetQuality.nonAggregatable.title', { + defaultMessage: 'Your request may take longer to complete', +}); + +const aggregationNotSupportedDescription = (dataset: string) => ( + + {dataset} + + ), + howToFixIt: ( + + {i18n.translate( + 'xpack.datasetQuality.flyout.nonAggregatableDatasets.link.title', + { + defaultMessage: 'rollover', + } + )} + + ), + }} + /> + ), + }} + /> + ), + }} + /> +); + +export function AggregationNotSupported({ dataStream }: { dataStream: string }) { + return ( + + + +

{aggregationNotSupportedDescription(dataStream)}

+
+
+
+ ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/columns.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/columns.tsx new file mode 100644 index 00000000000000..cfdf3b8d4a521c --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/columns.tsx @@ -0,0 +1,61 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { EuiBasicTableColumn } from '@elastic/eui'; +import type { FieldFormat } from '@kbn/field-formats-plugin/common'; +import { formatNumber } from '@elastic/eui'; + +import { DegradedField } from '../../../../../common/api_types'; +import { SparkPlot } from './spark_plot'; +import { NUMBER_FORMAT } from '../../../../../common/constants'; + +const fieldColumnName = i18n.translate('xpack.datasetQuality.details.degradedField.field', { + defaultMessage: 'Field', +}); + +const countColumnName = i18n.translate('xpack.datasetQuality.details.degradedField.count', { + defaultMessage: 'Docs count', +}); + +const lastOccurrenceColumnName = i18n.translate( + 'xpack.datasetQuality.details.degradedField.lastOccurrence', + { + defaultMessage: 'Last occurrence', + } +); + +export const getDegradedFieldsColumns = ({ + dateFormatter, + isLoading, +}: { + dateFormatter: FieldFormat; + isLoading: boolean; +}): Array> => [ + { + name: fieldColumnName, + field: 'name', + }, + { + name: countColumnName, + sortable: true, + field: 'count', + render: (_, { count, timeSeries }) => { + const countValue = formatNumber(count, NUMBER_FORMAT); + return ; + }, + }, + { + name: lastOccurrenceColumnName, + sortable: true, + field: 'lastOccurrence', + render: (lastOccurrence: number) => { + return dateFormatter.convert(lastOccurrence); + }, + }, +]; 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 new file mode 100644 index 00000000000000..01956405074e31 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/degraded_fields.tsx @@ -0,0 +1,67 @@ +/* + * 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 { + EuiFlexGroup, + EuiPanel, + EuiTitle, + EuiIconTip, + EuiAccordion, + useGeneratedHtmlId, + EuiBadge, + EuiBetaBadge, +} from '@elastic/eui'; +import { + overviewDegradedFieldsSectionTitle, + overviewDegradedFieldsSectionTitleTooltip, + overviewQualityIssuesAccordionTechPreviewBadge, +} from '../../../../../common/translations'; +import { DegradedFieldTable } from './table'; +import { useDegradedFields } from '../../../../hooks'; + +export function DegradedFields() { + const accordionId = useGeneratedHtmlId({ + prefix: overviewDegradedFieldsSectionTitle, + }); + + const { totalItemCount } = useDegradedFields(); + + const accordionTitle = ( + + +
{overviewDegradedFieldsSectionTitle}
+
+ + + {totalItemCount} + + +
+ ); + return ( + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/index.ts b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/index.ts new file mode 100644 index 00000000000000..7254285dc69bab --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/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 './degraded_fields'; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/spark_plot.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/spark_plot.tsx new file mode 100644 index 00000000000000..dcf1d4032c8860 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/spark_plot.tsx @@ -0,0 +1,101 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingChart, + euiPaletteColorBlind, + useEuiTheme, +} from '@elastic/eui'; +import { ScaleType, Settings, Tooltip, Chart, BarSeries } from '@elastic/charts'; +import { i18n } from '@kbn/i18n'; +import { Coordinate } from '../../../../../common/types'; + +export function SparkPlot({ + valueLabel, + isLoading, + series, +}: { + valueLabel: React.ReactNode; + isLoading: boolean; + series?: Coordinate[] | null; +}) { + return ( + + + + + {valueLabel} + + ); +} + +function SparkPlotItem({ + isLoading, + series, +}: { + isLoading: boolean; + series?: Coordinate[] | null; +}) { + const { euiTheme } = useEuiTheme(); + const chartSize = { + height: euiTheme.size.l, + width: '80px', + }; + + const commonStyle = { + ...chartSize, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }; + const palette = euiPaletteColorBlind({ rotations: 2 }); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (hasValidTimeSeries(series)) { + return ( +
+ + + + + +
+ ); + } + + return ( +
+ +
+ ); +} + +function hasValidTimeSeries(series?: Coordinate[] | null): series is Coordinate[] { + return !!series?.some((point) => point.y !== 0); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/table.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/table.tsx new file mode 100644 index 00000000000000..c8a6e623febab9 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/degraded_fields/table.tsx @@ -0,0 +1,54 @@ +/* + * 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 { EuiBasicTable, EuiEmptyPrompt } from '@elastic/eui'; +import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; +import { getDegradedFieldsColumns } from './columns'; +import { + overviewDegradedFieldsTableLoadingText, + overviewDegradedFieldsTableNoData, +} from '../../../../../common/translations'; +import { useDegradedFields } from '../../../../hooks/use_degraded_fields'; + +export const DegradedFieldTable = () => { + const { isLoading, pagination, renderedItems, onTableChange, sort, fieldFormats } = + useDegradedFields(); + const dateFormatter = fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.DATE, [ + ES_FIELD_TYPES.DATE, + ]); + const columns = getDegradedFieldsColumns({ dateFormatter, isLoading }); + + return ( + {overviewDegradedFieldsTableNoData}} + hasBorder={false} + titleSize="m" + /> + ) + } + /> + ); +}; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_docs_trend/degraded_docs_chart.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/degraded_docs_chart.tsx similarity index 89% rename from x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_docs_trend/degraded_docs_chart.tsx rename to x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/degraded_docs_chart.tsx index 9d13d0e3b26bab..ff311032fe69fc 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_docs_trend/degraded_docs_chart.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/degraded_docs_chart.tsx @@ -11,10 +11,10 @@ import { EuiFlexGroup, EuiLoadingChart, OnTimeChangeProps } from '@elastic/eui'; import { ViewMode } from '@kbn/embeddable-plugin/common'; import { KibanaErrorBoundary } from '@kbn/shared-ux-error-boundary'; -import { flyoutDegradedDocsTrendText } from '../../../../common/translations'; -import { TimeRangeConfig } from '../../../state_machines/dataset_quality_controller'; -import { useKibanaContextForPlugin } from '../../../utils'; -import { useDegradedDocsChart } from '../../../hooks'; +import { flyoutDegradedDocsTrendText } from '../../../../../../common/translations'; +import { useKibanaContextForPlugin } from '../../../../../utils'; +import { TimeRangeConfig } from '../../../../../../common/types'; +import { useDegradedDocs } from '../../../../../hooks/use_degraded_docs'; const CHART_HEIGHT = 180; const DISABLED_ACTIONS = [ @@ -26,7 +26,7 @@ const DISABLED_ACTIONS = [ interface DegradedDocsChartProps extends Pick< - ReturnType, + ReturnType, 'attributes' | 'isChartLoading' | 'onChartLoading' | 'extraActions' > { timeRange: TimeRangeConfig; 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 new file mode 100644 index 00000000000000..985a748e792b1e --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/index.tsx @@ -0,0 +1,147 @@ +/* + * 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, { useCallback, useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiAccordion, + EuiButtonIcon, + EuiCode, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiSkeletonRectangle, + EuiSpacer, + EuiTitle, + EuiToolTip, + OnTimeChangeProps, + useGeneratedHtmlId, +} from '@elastic/eui'; +import type { DataViewField } from '@kbn/data-views-plugin/common'; +import { css } from '@emotion/react'; +import { UnifiedBreakdownFieldSelector } from '@kbn/unified-histogram-plugin/public'; +import { + openInLogsExplorerText, + overviewDegradedDocsText, +} from '../../../../../../common/translations'; +import { useDegradedDocs } from '../../../../../hooks/use_degraded_docs'; +import { DegradedDocsChart } from './degraded_docs_chart'; +import { + useDatasetQualityDetailsRedirectLink, + useDatasetQualityDetailsState, +} from '../../../../../hooks'; +import { _IGNORED } from '../../../../../../common/es_fields'; + +const degradedDocsTooltip = ( + + _ignored + + ), + }} + /> +); + +// Allow for lazy loading +// eslint-disable-next-line import/no-default-export +export default function DegradedDocs({ lastReloadTime }: { lastReloadTime: number }) { + const { timeRange, updateTimeRange, datasetDetails } = useDatasetQualityDetailsState(); + const { dataView, breakdown, ...chartProps } = useDegradedDocs(); + + const accordionId = useGeneratedHtmlId({ + prefix: overviewDegradedDocsText, + }); + + const [breakdownDataViewField, setBreakdownDataViewField] = useState( + undefined + ); + + const degradedDocLinkLogsExplorer = useDatasetQualityDetailsRedirectLink({ + dataStreamStat: datasetDetails, + timeRangeConfig: timeRange, + query: { language: 'kuery', query: `${_IGNORED}: *` }, + }); + + useEffect(() => { + if (breakdown.dataViewField && breakdown.fieldSupportsBreakdown) { + setBreakdownDataViewField(breakdown.dataViewField); + } else { + setBreakdownDataViewField(undefined); + } + }, [setBreakdownDataViewField, breakdown]); + + const onTimeRangeChange = useCallback( + ({ start, end }: Pick) => { + updateTimeRange({ start, end, refreshInterval: timeRange.refresh.value }); + }, + [updateTimeRange, timeRange.refresh] + ); + + const accordionTitle = ( + + +
{overviewDegradedDocsText}
+
+ + + +
+ ); + + return ( + + + + + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_docs_trend/lens_attributes.ts b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/lens_attributes.ts similarity index 97% rename from x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_docs_trend/lens_attributes.ts rename to x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/lens_attributes.ts index a44b9a562a428e..f089ef5d74c242 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_docs_trend/lens_attributes.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/document_trends/degraded_docs/lens_attributes.ts @@ -6,13 +6,13 @@ */ import { i18n } from '@kbn/i18n'; -import { GenericIndexPatternColumn, TypedLensByValueInput } from '@kbn/lens-plugin/public'; +import type { GenericIndexPatternColumn, TypedLensByValueInput } from '@kbn/lens-plugin/public'; import { v4 as uuidv4 } from 'uuid'; import { flyoutDegradedDocsPercentageText, flyoutDegradedDocsTrendText, -} from '../../../../common/translations'; +} from '../../../../../../common/translations'; enum DatasetQualityLensColumn { Date = 'date_column', @@ -268,7 +268,7 @@ function getChartColumns(breakdownField?: string): Record - i18n.translate('xpack.datasetQuality.flyoutDegradedDocsTopNValues', { + i18n.translate('xpack.datasetQuality.details.degradedDocsTopNValues', { defaultMessage: 'Top {count} values of {fieldName}', values: { count, fieldName }, description: diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/header.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/header.tsx new file mode 100644 index 00000000000000..a47ed1764049cb --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/header.tsx @@ -0,0 +1,78 @@ +/* + * 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 { + EuiFlexGroup, + EuiIcon, + EuiSuperDatePicker, + EuiTitle, + EuiToolTip, + OnRefreshProps, + OnTimeChangeProps, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import React, { useCallback } from 'react'; +import { useDatasetQualityDetailsState } from '../../../hooks'; +import { overviewHeaderTitle, overviewTitleTooltip } from '../../../../common/translations'; + +// Allow for lazy loading +// eslint-disable-next-line import/no-default-export +export default function OverviewHeader({ + handleRefresh, +}: { + handleRefresh: (refreshProps: OnRefreshProps) => void; +}) { + const { timeRange, updateTimeRange } = useDatasetQualityDetailsState(); + + const onTimeChange = useCallback( + ({ isInvalid, ...timeRangeProps }: OnTimeChangeProps) => { + if (!isInvalid) { + updateTimeRange({ refreshInterval: timeRange.refresh.value, ...timeRangeProps }); + } + }, + [updateTimeRange, timeRange.refresh] + ); + + return ( + + + + {overviewHeaderTitle} + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/index.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/index.tsx new file mode 100644 index 00000000000000..380dd6bf09b957 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/index.tsx @@ -0,0 +1,42 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { dynamic } from '@kbn/shared-ux-utility'; +import { EuiSpacer, OnRefreshProps } from '@elastic/eui'; +import { useDatasetQualityDetailsState } from '../../../hooks'; +import { AggregationNotSupported } from './aggregation_not_supported'; +import { DegradedFields } from './degraded_fields'; + +const OverviewHeader = dynamic(() => import('./header')); +const Summary = dynamic(() => import('./summary')); +const DegradedDocs = dynamic(() => import('./document_trends/degraded_docs')); + +export function Overview() { + const { dataStream, isNonAggregatable, updateTimeRange } = useDatasetQualityDetailsState(); + const [lastReloadTime, setLastReloadTime] = useState(Date.now()); + + const handleRefresh = useCallback( + (refreshProps: OnRefreshProps) => { + updateTimeRange(refreshProps); + setLastReloadTime(Date.now()); + }, + [updateTimeRange] + ); + return ( + <> + {isNonAggregatable && } + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/summary/index.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/summary/index.tsx new file mode 100644 index 00000000000000..707c900a248a1a --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/summary/index.tsx @@ -0,0 +1,74 @@ +/* + * 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 { EuiFlexGroup } from '@elastic/eui'; +import { Panel, PanelIndicator } from './panel'; +import { + overviewPanelDatasetQualityIndicatorDegradedDocs, + overviewPanelDocumentsIndicatorSize, + overviewPanelDocumentsIndicatorTotalCount, + overviewPanelResourcesIndicatorServices, + overviewPanelResourcesIndicatorSize, + overviewPanelTitleDatasetQuality, + overviewPanelTitleDocuments, + overviewPanelTitleResources, +} from '../../../../../common/translations'; +import { useOverviewSummaryPanel } from '../../../../hooks/use_overview_summary_panel'; + +// Allow for lazy loading +// eslint-disable-next-line import/no-default-export +export default function Summary() { + const { + isSummaryPanelLoading, + totalDocsCount, + sizeInBytesAvailable, + sizeInBytes, + isUserAllowedToSeeSizeInBytes, + totalServicesCount, + totalHostsCount, + totalDegradedDocsCount, + } = useOverviewSummaryPanel(); + return ( + + + + {sizeInBytesAvailable && ( + + )} + + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/summary/panel.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/summary/panel.tsx new file mode 100644 index 00000000000000..b40f563a4dff47 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/dataset_quality_details/overview/summary/panel.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSkeletonTitle, EuiText } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import { PrivilegesWarningIconWrapper } from '../../../common'; + +const verticalRule = css` + width: 1px; + height: 65px; + background-color: ${euiThemeVars.euiColorLightShade}; +`; + +const verticalRuleHidden = css` + width: 1px; + height: 65px; + visibility: hidden; +`; + +export function Panel({ + title, + secondaryTitle, + children, +}: { + title: string; + secondaryTitle?: React.ReactNode; + children: React.ReactNode | React.ReactNode[]; +}) { + const renderChildrenWithSeparator = (panelChildren: React.ReactNode | React.ReactNode[]) => { + if (Array.isArray(panelChildren)) { + return panelChildren.map((panelChild, index) => ( + + {panelChild} + {index < panelChildren.length - 1 && } + + )); + } + return ( + <> + {panelChildren} + + + ); + }; + return ( + + + + + +
{title}
+
+
+ {secondaryTitle && {secondaryTitle}} +
+
+ + {renderChildrenWithSeparator(children)} + +
+ ); +} + +export function PanelIndicator({ + label, + value, + isLoading, + userHasPrivilege = true, +}: { + label: string; + value: string | number; + isLoading: boolean; + userHasPrivilege?: boolean; +}) { + return ( + + {isLoading ? ( + + ) : ( + <> + + {label} + + + <> + + {userHasPrivilege && ( + +

{value}

+
+ )} + + )} +
+ ); +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/dataset_summary.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/dataset_summary.tsx index 052f4b63f0da60..cb35e9c74d3343 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/dataset_summary.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/dataset_summary.tsx @@ -10,9 +10,9 @@ import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import { DataStreamDetails, DataStreamSettings } from '../../../common/data_streams_stats'; import { - flyoutDatasetCreatedOnText, + datasetCreatedOnText, flyoutDatasetDetailsText, - flyoutDatasetLastActivityText, + datasetLastActivityText, } from '../../../common/translations'; import { FieldsList, FieldsListLoading } from './fields_list'; @@ -46,12 +46,12 @@ export function DatasetSummary({ title={flyoutDatasetDetailsText} fields={[ { - fieldTitle: flyoutDatasetLastActivityText, + fieldTitle: datasetLastActivityText, fieldValue: formattedLastActivity, isLoading: dataStreamDetailsLoading, }, { - fieldTitle: flyoutDatasetCreatedOnText, + fieldTitle: datasetCreatedOnText, fieldValue: formattedCreatedOn, isLoading: dataStreamSettingsLoading, }, diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_docs_trend/degraded_docs.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_docs_trend/degraded_docs.tsx index c1c6bdf23f7c4b..5335d5de8692d6 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_docs_trend/degraded_docs.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_docs_trend/degraded_docs.tsx @@ -25,9 +25,9 @@ import type { DataViewField } from '@kbn/data-views-plugin/common'; import { useDegradedDocsChart } from '../../../hooks'; import { DEFAULT_TIME_RANGE, DEFAULT_DATEPICKER_REFRESH } from '../../../../common/constants'; -import { flyoutDegradedDocsText } from '../../../../common/translations'; -import { TimeRangeConfig } from '../../../state_machines/dataset_quality_controller'; -import { DegradedDocsChart } from './degraded_docs_chart'; +import { overviewDegradedDocsText } from '../../../../common/translations'; +import { DegradedDocsChart } from '../../dataset_quality_details/overview/document_trends/degraded_docs/degraded_docs_chart'; +import { TimeRangeConfig } from '../../../../common/types'; export function DegradedDocs({ dataStream, @@ -70,7 +70,7 @@ export function DegradedDocs({ `} > -
{flyoutDegradedDocsText}
+
{overviewDegradedDocsText}
diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_fields/table.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_fields/table.tsx index 9820e1722d0fc8..1a1baa8aae7cdf 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_fields/table.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/degraded_fields/table.tsx @@ -11,8 +11,8 @@ import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import { useDatasetQualityDegradedField } from '../../../hooks'; import { getDegradedFieldsColumns } from './columns'; import { - flyoutDegradedFieldsTableLoadingText, - flyoutDegradedFieldsTableNoData, + overviewDegradedFieldsTableLoadingText, + overviewDegradedFieldsTableNoData, } from '../../../../common/translations'; export const DegradedFieldTable = () => { @@ -38,12 +38,12 @@ export const DegradedFieldTable = () => { }} noItemsMessage={ isLoading ? ( - flyoutDegradedFieldsTableLoadingText + overviewDegradedFieldsTableLoadingText ) : ( {flyoutDegradedFieldsTableNoData}} + title={

{overviewDegradedFieldsTableNoData}

} hasBorder={false} titleSize="m" /> diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout.tsx index 7dc455b2804440..cb6944057a2d62 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout.tsx @@ -19,15 +19,17 @@ import { EuiPanel, EuiSkeletonRectangle, } from '@elastic/eui'; +import { dynamic } from '@kbn/shared-ux-utility'; import { flyoutCancelText } from '../../../common/translations'; import { useDatasetQualityFlyout, useDatasetDetailsTelemetry } from '../../hooks'; import { DatasetSummary, DatasetSummaryLoading } from './dataset_summary'; import { Header } from './header'; -import { IntegrationSummary } from './integration_summary'; import { FlyoutProps } from './types'; -import { FlyoutSummary } from './flyout_summary/flyout_summary'; import { BasicDataStream } from '../../../common/types'; +const FlyoutSummary = dynamic(() => import('./flyout_summary/flyout_summary')); +const IntegrationSummary = dynamic(() => import('./integration_summary')); + // Allow for lazy loading // eslint-disable-next-line import/no-default-export export default function Flyout({ dataset, closeFlyout }: FlyoutProps) { @@ -74,6 +76,7 @@ export default function Flyout({ dataset, closeFlyout }: FlyoutProps) { linkDetails={linkDetails} loading={!loadingState.datasetIntegrationDone} title={title} + timeRange={timeRange} /> diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary.tsx index 353e7f353b6dbd..5b89c43ad92d1c 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary.tsx @@ -23,10 +23,11 @@ import { DegradedDocs } from '../degraded_docs_trend/degraded_docs'; import { DataStreamDetails } from '../../../../common/api_types'; import { DEFAULT_TIME_RANGE, DEFAULT_DATEPICKER_REFRESH } from '../../../../common/constants'; import { useDatasetQualityContext } from '../../dataset_quality/context'; -import { FlyoutDataset, TimeRangeConfig } from '../../../state_machines/dataset_quality_controller'; import { FlyoutSummaryHeader } from './flyout_summary_header'; import { FlyoutSummaryKpis, FlyoutSummaryKpisLoading } from './flyout_summary_kpis'; import { DegradedFields } from '../degraded_fields/degraded_fields'; +import { TimeRangeConfig } from '../../../../common/types'; +import { FlyoutDataset } from '../../../state_machines/dataset_quality_controller'; const nonAggregatableWarningTitle = i18n.translate('xpack.datasetQuality.nonAggregatable.title', { defaultMessage: 'Your request may take longer to complete', @@ -77,7 +78,9 @@ const nonAggregatableWarningDescription = (dataset: string) => ( /> ); -export function FlyoutSummary({ +// Allow for lazy loading +// eslint-disable-next-line import/no-default-export +export default function FlyoutSummary({ dataStream, dataStreamStat, dataStreamDetails, diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary_header.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary_header.tsx index d449eb35536478..c0ee7303b51a54 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary_header.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary_header.tsx @@ -19,7 +19,7 @@ import { import { FormattedMessage } from '@kbn/i18n-react'; import { flyoutSummaryText } from '../../../../common/translations'; -import { TimeRangeConfig } from '../../../state_machines/dataset_quality_controller'; +import { TimeRangeConfig } from '../../../../common/types'; export function FlyoutSummaryHeader({ timeRange, diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary_kpis.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary_kpis.tsx index 9b0ca4dfd12775..47b41712dc7f51 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary_kpis.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/flyout_summary_kpis.tsx @@ -14,9 +14,10 @@ import { DataStreamDetails } from '../../../../common/api_types'; import { useKibanaContextForPlugin } from '../../../utils'; import { NavigationSource } from '../../../services/telemetry'; import { useDatasetDetailsTelemetry, useRedirectLink } from '../../../hooks'; -import { FlyoutDataset, TimeRangeConfig } from '../../../state_machines/dataset_quality_controller'; +import { FlyoutDataset } from '../../../state_machines/dataset_quality_controller'; import { FlyoutSummaryKpiItem, FlyoutSummaryKpiItemLoading } from './flyout_summary_kpi_item'; import { getSummaryKpis } from './get_summary_kpis'; +import { TimeRangeConfig } from '../../../../common/types'; export function FlyoutSummaryKpis({ dataStreamStat, diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/get_summary_kpis.test.ts b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/get_summary_kpis.test.ts index 5807c21a6a2517..28f5334e2f1995 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/get_summary_kpis.test.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/get_summary_kpis.test.ts @@ -8,7 +8,6 @@ import { formatNumber } from '@elastic/eui'; import type { useKibanaContextForPlugin } from '../../../utils'; import type { useDatasetDetailsTelemetry } from '../../../hooks'; -import { TimeRangeConfig } from '../../../state_machines/dataset_quality_controller'; import { BYTE_NUMBER_FORMAT, @@ -17,7 +16,7 @@ import { MAX_HOSTS_METRIC_VALUE, } from '../../../../common/constants'; import { - flyoutDegradedDocsText, + overviewDegradedDocsText, flyoutDocsCountTotalText, flyoutHostsText, flyoutServicesText, @@ -25,6 +24,7 @@ import { flyoutSizeText, } from '../../../../common/translations'; import { getSummaryKpis } from './get_summary_kpis'; +import { TimeRangeConfig } from '../../../../common/types'; const dataStreamDetails = { services: { @@ -83,7 +83,7 @@ describe('getSummaryKpis', () => { { title: flyoutSizeText, value: formatNumber(dataStreamDetails.sizeBytes ?? 0, BYTE_NUMBER_FORMAT), - userHasPrivilege: true, + userHasPrivilege: false, }, { title: flyoutServicesText, @@ -98,7 +98,7 @@ describe('getSummaryKpis', () => { userHasPrivilege: true, }, { - title: flyoutDegradedDocsText, + title: overviewDegradedDocsText, value: '200', link: { label: flyoutShowAllText, @@ -143,7 +143,7 @@ describe('getSummaryKpis', () => { { title: flyoutSizeText, value: formatNumber(dataStreamDetails.sizeBytes ?? 0, BYTE_NUMBER_FORMAT), - userHasPrivilege: true, + userHasPrivilege: false, }, { title: flyoutServicesText, @@ -158,7 +158,7 @@ describe('getSummaryKpis', () => { userHasPrivilege: true, }, { - title: flyoutDegradedDocsText, + title: overviewDegradedDocsText, value: '200', link: { label: flyoutShowAllText, diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/get_summary_kpis.ts b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/get_summary_kpis.ts index 563c7d06cea48e..b574e1e8a8160c 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/get_summary_kpis.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/flyout_summary/get_summary_kpis.ts @@ -15,7 +15,7 @@ import { NUMBER_FORMAT, } from '../../../../common/constants'; import { - flyoutDegradedDocsText, + overviewDegradedDocsText, flyoutDocsCountTotalText, flyoutHostsText, flyoutServicesText, @@ -26,7 +26,7 @@ import { DataStreamDetails } from '../../../../common/api_types'; import { NavigationTarget, NavigationSource } from '../../../services/telemetry'; import { useKibanaContextForPlugin } from '../../../utils'; import type { useRedirectLink, useDatasetDetailsTelemetry } from '../../../hooks'; -import { TimeRangeConfig } from '../../../state_machines/dataset_quality_controller'; +import { TimeRangeConfig } from '../../../../common/types'; export function getSummaryKpis({ dataStreamDetails, @@ -77,7 +77,7 @@ export function getSummaryKpis({ { title: flyoutSizeText, value: formatNumber(dataStreamDetails?.sizeBytes ?? 0, BYTE_NUMBER_FORMAT), - userHasPrivilege: dataStreamDetails?.userPrivileges?.canMonitor ?? true, + userHasPrivilege: Boolean(dataStreamDetails?.userPrivileges?.canMonitor), }, ] : []), @@ -89,7 +89,7 @@ export function getSummaryKpis({ }, getHostsKpi(dataStreamDetails?.hosts, timeRange, telemetry, hostsLocator), { - title: flyoutDegradedDocsText, + title: overviewDegradedDocsText, value: formatNumber(dataStreamDetails?.degradedDocsCount ?? 0, NUMBER_FORMAT), link: degradedDocsLinkProps && degradedDocsLinkProps.linkProps.href diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/header.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/header.tsx index 10d0c96a057bd3..1117c7da12a1b2 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/header.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/header.tsx @@ -17,23 +17,22 @@ import { } from '@elastic/eui'; import { css } from '@emotion/react'; import React from 'react'; -import { - flyoutOpenInDiscoverText, - flyoutOpenInLogsExplorerText, -} from '../../../common/translations'; +import { openInDiscoverText, openInLogsExplorerText } from '../../../common/translations'; import { NavigationSource } from '../../services/telemetry'; import { useRedirectLink } from '../../hooks'; import { IntegrationIcon } from '../common'; -import { BasicDataStream } from '../../../common/types'; +import { BasicDataStream, TimeRangeConfig } from '../../../common/types'; export function Header({ linkDetails, loading, title, + timeRange, }: { linkDetails: BasicDataStream; loading: boolean; title: string; + timeRange: TimeRangeConfig; }) { const { integration } = linkDetails; const euiShadow = useEuiShadow('s'); @@ -44,6 +43,7 @@ export function Header({ page: 'details', navigationSource: NavigationSource.Header, }, + timeRangeConfig: timeRange, }); return ( @@ -90,8 +90,8 @@ export function Header({ } > {redirectLinkProps.isLogsExplorerAvailable - ? flyoutOpenInLogsExplorerText - : flyoutOpenInDiscoverText} + ? openInLogsExplorerText + : openInDiscoverText} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/integration_summary.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/integration_summary.tsx index 6ac94411098a64..53262c7821ce73 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/integration_summary.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/components/flyout/integration_summary.tsx @@ -11,7 +11,7 @@ import { css } from '@emotion/react'; import { flyoutIntegrationDetailsText, flyoutIntegrationNameText, - flyoutIntegrationVersionText, + integrationVersionText, } from '../../../common/translations'; import { IntegrationIcon } from '../common'; import { FieldsList } from './fields_list'; @@ -19,7 +19,9 @@ import { IntegrationActionsMenu } from './integration_actions_menu'; import { Integration } from '../../../common/data_streams_stats/integration'; import { Dashboard } from '../../../common/api_types'; -export function IntegrationSummary({ +// Allow for lazy loading +// eslint-disable-next-line import/no-default-export +export default function IntegrationSummary({ integration, dashboards, dashboardsLoading, @@ -60,7 +62,7 @@ export function IntegrationSummary({ isLoading: false, }, { - fieldTitle: flyoutIntegrationVersionText, + fieldTitle: integrationVersionText, fieldValue: version, isLoading: false, }, diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/controller/create_controller.ts b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/create_controller.ts similarity index 88% rename from x-pack/plugins/observability_solution/dataset_quality/public/controller/create_controller.ts rename to x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/create_controller.ts index 3d8e808adfcfb7..7424e13b0f93cb 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/controller/create_controller.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/create_controller.ts @@ -10,13 +10,13 @@ import { getDevToolsOptions } from '@kbn/xstate-utils'; import equal from 'fast-deep-equal'; import { distinctUntilChanged, from, map } from 'rxjs'; import { interpret } from 'xstate'; -import { IDataStreamsStatsClient } from '../services/data_streams_stats'; -import { IDataStreamDetailsClient } from '../services/data_stream_details'; +import { IDataStreamsStatsClient } from '../../services/data_streams_stats'; +import { IDataStreamDetailsClient } from '../../services/data_stream_details'; import { createDatasetQualityControllerStateMachine, DEFAULT_CONTEXT, -} from '../state_machines/dataset_quality_controller'; -import { DatasetQualityStartDeps } from '../types'; +} from '../../state_machines/dataset_quality_controller'; +import { DatasetQualityStartDeps } from '../../types'; import { getContextFromPublicState, getPublicStateFromContext } from './public_state'; import { DatasetQualityController, DatasetQualityPublicStateUpdate } from './types'; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/controller/index.ts b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/index.ts similarity index 100% rename from x-pack/plugins/observability_solution/dataset_quality/public/controller/index.ts rename to x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/index.ts diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/controller/lazy_create_controller.ts b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/lazy_create_controller.ts similarity index 100% rename from x-pack/plugins/observability_solution/dataset_quality/public/controller/lazy_create_controller.ts rename to x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/lazy_create_controller.ts diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/controller/public_state.ts b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/public_state.ts similarity index 96% rename from x-pack/plugins/observability_solution/dataset_quality/public/controller/public_state.ts rename to x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/public_state.ts index 9500b473c95edc..1bf0088bc7a485 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/controller/public_state.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/public_state.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { DatasetTableSortField, DegradedFieldSortField } from '../hooks'; +import { DatasetTableSortField, DegradedFieldSortField } from '../../hooks'; import { DatasetQualityControllerContext, DEFAULT_CONTEXT, -} from '../state_machines/dataset_quality_controller'; +} from '../../state_machines/dataset_quality_controller'; import { DatasetQualityPublicState, DatasetQualityPublicStateUpdate } from './types'; export const getPublicStateFromContext = ( diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/controller/types.ts b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/types.ts similarity index 96% rename from x-pack/plugins/observability_solution/dataset_quality/public/controller/types.ts rename to x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/types.ts index 66757d34095673..decb7454bd193b 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/controller/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality/types.ts @@ -12,7 +12,7 @@ import { WithFlyoutOptions, WithTableOptions, DegradedFields, -} from '../state_machines/dataset_quality_controller'; +} from '../../state_machines/dataset_quality_controller'; export interface DatasetQualityController { state$: Observable; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/create_controller.ts b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/create_controller.ts new file mode 100644 index 00000000000000..d51f60ef93c714 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/create_controller.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from '@kbn/core/public'; +import { getDevToolsOptions } from '@kbn/xstate-utils'; +import equal from 'fast-deep-equal'; +import { distinctUntilChanged, from, map } from 'rxjs'; +import { interpret } from 'xstate'; +import { createDatasetQualityDetailsControllerStateMachine } from '../../state_machines/dataset_quality_details_controller/state_machine'; +import { IDataStreamsStatsClient } from '../../services/data_streams_stats'; +import { IDataStreamDetailsClient } from '../../services/data_stream_details'; +import { DatasetQualityStartDeps } from '../../types'; +import { getContextFromPublicState, getPublicStateFromContext } from './public_state'; +import { DatasetQualityDetailsController, DatasetQualityDetailsPublicStateUpdate } from './types'; + +interface Dependencies { + core: CoreStart; + plugins: DatasetQualityStartDeps; + dataStreamStatsClient: IDataStreamsStatsClient; + dataStreamDetailsClient: IDataStreamDetailsClient; +} + +export const createDatasetQualityDetailsControllerFactory = + ({ core, plugins, dataStreamStatsClient, dataStreamDetailsClient }: Dependencies) => + async ({ + initialState, + }: { + initialState: DatasetQualityDetailsPublicStateUpdate; + }): Promise => { + const initialContext = getContextFromPublicState(initialState); + + const machine = createDatasetQualityDetailsControllerStateMachine({ + initialContext, + plugins, + toasts: core.notifications.toasts, + dataStreamStatsClient, + dataStreamDetailsClient, + }); + + const service = interpret(machine, { + devTools: getDevToolsOptions(), + }); + + const state$ = from(service).pipe( + map(({ context }) => getPublicStateFromContext(context)), + distinctUntilChanged(equal) + ); + + return { + state$, + service, + }; + }; + +export type CreateDatasetQualityDetailsControllerFactory = + typeof createDatasetQualityDetailsControllerFactory; +export type CreateDatasetQualityDetailsController = ReturnType< + typeof createDatasetQualityDetailsControllerFactory +>; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/index.ts b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/index.ts new file mode 100644 index 00000000000000..d0e19da1736db0 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './create_controller'; +export * from './types'; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/lazy_create_controller.ts b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/lazy_create_controller.ts new file mode 100644 index 00000000000000..d134ce09b93981 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/lazy_create_controller.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CreateDatasetQualityDetailsControllerFactory } from './create_controller'; + +export const createDatasetQualityDetailsControllerLazyFactory: CreateDatasetQualityDetailsControllerFactory = + (dependencies) => async (args) => { + const { createDatasetQualityDetailsControllerFactory } = await import('./create_controller'); + + return createDatasetQualityDetailsControllerFactory(dependencies)(args); + }; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/public_state.ts b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/public_state.ts new file mode 100644 index 00000000000000..75f3337103e6f1 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/public_state.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 { DegradedFieldSortField } from '../../hooks'; +import { + DatasetQualityDetailsControllerContext, + DEFAULT_CONTEXT, +} from '../../state_machines/dataset_quality_details_controller'; +import { DatasetQualityDetailsPublicState, DatasetQualityDetailsPublicStateUpdate } from './types'; + +export const getPublicStateFromContext = ( + context: DatasetQualityDetailsControllerContext +): DatasetQualityDetailsPublicState => { + return { + dataStream: context.dataStream, + degradedFields: context.degradedFields, + timeRange: context.timeRange, + breakdownField: context.breakdownField, + integration: context.integration, + }; +}; + +export const getContextFromPublicState = ( + publicState: DatasetQualityDetailsPublicStateUpdate +): DatasetQualityDetailsControllerContext => ({ + ...DEFAULT_CONTEXT, + degradedFields: { + table: { + ...DEFAULT_CONTEXT.degradedFields.table, + ...publicState.degradedFields?.table, + sort: publicState.degradedFields?.table?.sort + ? { + ...publicState.degradedFields.table.sort, + field: publicState.degradedFields.table.sort.field as DegradedFieldSortField, + } + : DEFAULT_CONTEXT.degradedFields.table.sort, + }, + }, + timeRange: { + ...DEFAULT_CONTEXT.timeRange, + ...publicState.timeRange, + refresh: { + ...DEFAULT_CONTEXT.timeRange.refresh, + ...publicState.timeRange?.refresh, + }, + }, + dataStream: publicState.dataStream, +}); diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/types.ts b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/types.ts new file mode 100644 index 00000000000000..65ca53c073d42c --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/controller/dataset_quality_details/types.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 { Observable } from 'rxjs'; +import { + DatasetQualityDetailsControllerStateService, + DegradedFieldsTableConfig, + WithDefaultControllerState, +} from '../../state_machines/dataset_quality_details_controller'; + +type DegradedFieldTableSortOptions = Omit & { + field: string; +}; + +export type DatasetQualityDegradedFieldTableOptions = Partial< + Omit & { + sort?: DegradedFieldTableSortOptions; + } +>; + +export type DatasetQualityDetailsPublicState = WithDefaultControllerState; + +// This type is used by external consumers where it enforces datastream to be +// a must and everything else can be optional. The table inside the +// degradedFields must accept field property as string +export type DatasetQualityDetailsPublicStateUpdate = Partial< + Pick +> & { + dataStream: string; +} & { + degradedFields?: { + table?: DatasetQualityDegradedFieldTableOptions; + }; +}; + +export interface DatasetQualityDetailsController { + state$: Observable; + service: DatasetQualityDetailsControllerStateService; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/index.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/index.ts index 0fc332e68bf8da..5c746e2f1177b2 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/index.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/index.ts @@ -13,3 +13,7 @@ export * from './use_summary_panel'; export * from './use_create_dataview'; export * from './use_dataset_quality_degraded_field'; export * from './use_telemetry'; +export * from './use_dataset_quality_details_state'; +export * from './use_dataset_quality_details_redirect_link'; +export * from './use_degraded_fields'; +export * from './use_integration_actions'; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_degraded_field.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_degraded_field.ts index ed95a0cd7fec98..d92aa5be153f66 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_degraded_field.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_degraded_field.ts @@ -16,8 +16,9 @@ import { } from '../../common/constants'; import { useKibanaContextForPlugin } from '../utils'; -export type DegradedFieldSortField = keyof DegradedField; +type DegradedFieldSortField = keyof DegradedField; +// TODO: DELETE this hook in favour of new hook post migration export function useDatasetQualityDegradedField() { const { service } = useDatasetQualityContext(); const { diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_details_redirect_link.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_details_redirect_link.ts new file mode 100644 index 00000000000000..3000d05aa34de1 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_details_redirect_link.ts @@ -0,0 +1,188 @@ +/* + * 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 { + SINGLE_DATASET_LOCATOR_ID, + type SingleDatasetLocatorParams, +} from '@kbn/deeplinks-observability'; +import { type DiscoverAppLocatorParams, DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common'; +import { type Query, type AggregateQuery, buildPhraseFilter } from '@kbn/es-query'; +import { getRouterLinkProps } from '@kbn/router-utils'; +import type { RouterLinkProps } from '@kbn/router-utils/src/get_router_link_props'; +import type { LocatorPublic } from '@kbn/share-plugin/common'; +import type { LocatorClient } from '@kbn/shared-ux-prompt-no-data-views-types'; +import { useKibanaContextForPlugin } from '../utils'; +import { BasicDataStream, TimeRangeConfig } from '../../common/types'; + +export const useDatasetQualityDetailsRedirectLink = ({ + dataStreamStat, + query, + timeRangeConfig, + breakdownField, +}: { + dataStreamStat: T; + query?: Query | AggregateQuery; + timeRangeConfig: TimeRangeConfig; + breakdownField?: string; +}) => { + const { + services: { share }, + } = useKibanaContextForPlugin(); + + const { from, to } = timeRangeConfig; + + const logsExplorerLocator = + share.url.locators.get(SINGLE_DATASET_LOCATOR_ID); + + const config = logsExplorerLocator + ? buildLogsExplorerConfig({ + locator: logsExplorerLocator, + dataStreamStat, + query, + from, + to, + breakdownField, + }) + : buildDiscoverConfig({ + locatorClient: share.url.locators, + dataStreamStat, + query, + from, + to, + breakdownField, + }); + + return { + linkProps: { + ...config.routerLinkProps, + }, + navigate: config.navigate, + isLogsExplorerAvailable: !!logsExplorerLocator, + }; +}; + +const buildLogsExplorerConfig = ({ + locator, + dataStreamStat, + query, + from, + to, + breakdownField, +}: { + locator: LocatorPublic; + dataStreamStat: T; + query?: Query | AggregateQuery; + from: string; + to: string; + breakdownField?: string; +}): { + navigate: () => void; + routerLinkProps: RouterLinkProps; +} => { + const params: SingleDatasetLocatorParams = { + dataset: dataStreamStat.name, + timeRange: { + from, + to, + }, + integration: dataStreamStat.integration?.name, + query, + filterControls: { + namespace: { + mode: 'include', + values: [dataStreamStat.namespace], + }, + }, + breakdownField, + }; + + const urlToLogsExplorer = locator.getRedirectUrl(params); + + const navigateToLogsExplorer = () => { + locator.navigate(params) as Promise; + }; + + const logsExplorerLinkProps = getRouterLinkProps({ + href: urlToLogsExplorer, + onClick: navigateToLogsExplorer, + }); + + return { routerLinkProps: logsExplorerLinkProps, navigate: navigateToLogsExplorer }; +}; + +const buildDiscoverConfig = ({ + locatorClient, + dataStreamStat, + query, + from, + to, + breakdownField, +}: { + locatorClient: LocatorClient; + dataStreamStat: T; + query?: Query | AggregateQuery; + from: string; + to: string; + breakdownField?: string; +}): { + navigate: () => void; + routerLinkProps: RouterLinkProps; +} => { + const dataViewId = `${dataStreamStat.type}-${dataStreamStat.name}-*`; + const dataViewTitle = dataStreamStat.integration + ? `[${dataStreamStat.integration.title}] ${dataStreamStat.name}` + : `${dataViewId}`; + + const params: DiscoverAppLocatorParams = { + timeRange: { + from, + to, + }, + refreshInterval: { + pause: true, + value: 60000, + }, + dataViewId, + dataViewSpec: { + id: dataViewId, + title: dataViewTitle, + }, + query, + breakdownField, + columns: ['@timestamp', 'message'], + filters: [ + buildPhraseFilter( + { + name: 'data_stream.namespace', + type: 'string', + }, + dataStreamStat.namespace, + { + id: dataViewId, + title: dataViewTitle, + } + ), + ], + interval: 'auto', + sort: [['@timestamp', 'desc']], + }; + + const locator = locatorClient.get(DISCOVER_APP_LOCATOR); + + const urlToDiscover = locator?.getRedirectUrl(params); + + const navigateToDiscover = () => { + locator?.navigate(params) as Promise; + }; + + const discoverLinkProps = getRouterLinkProps({ + href: urlToDiscover, + onClick: navigateToDiscover, + }); + + return { routerLinkProps: discoverLinkProps, navigate: navigateToDiscover }; +}; 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 new file mode 100644 index 00000000000000..4b0626f9515802 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_details_state.ts @@ -0,0 +1,135 @@ +/* + * 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 { useCallback } from 'react'; +import { useSelector } from '@xstate/react'; +import { OnRefreshProps } from '@elastic/eui'; +import { DEFAULT_DATEPICKER_REFRESH } from '../../common/constants'; +import { useDatasetQualityDetailsContext } from '../components/dataset_quality_details/context'; +import { indexNameToDataStreamParts } from '../../common/utils'; +import { BasicDataStream } from '../../common/types'; +import { useKibanaContextForPlugin } from '../utils'; + +export const useDatasetQualityDetailsState = () => { + const { service } = useDatasetQualityDetailsContext(); + + const { + services: { fieldFormats }, + } = useKibanaContextForPlugin(); + + const { dataStream, degradedFields, timeRange, breakdownField, isIndexNotFoundError } = + useSelector(service, (state) => state.context) ?? {}; + + const isNonAggregatable = useSelector(service, (state) => + state.matches('initializing.nonAggregatableDataset.done') + ? state.context.isNonAggregatable + : false + ); + + const isBreakdownFieldEcs = useSelector(service, (state) => + state.matches('initializing.checkBreakdownFieldIsEcs.done') + ? state.context.isBreakdownFieldEcs + : false + ); + + const dataStreamSettings = useSelector(service, (state) => + state.matches('initializing.dataStreamSettings.initializeIntegrations') + ? state.context.dataStreamSettings + : undefined + ); + + const integrationDetails = { + integration: useSelector(service, (state) => + state.matches( + 'initializing.dataStreamSettings.initializeIntegrations.integrationDetails.done' + ) + ? state.context.integration + : undefined + ), + dashboard: useSelector(service, (state) => + state.matches( + 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.done' + ) + ? state.context.integrationDashboards + : undefined + ), + }; + + const canUserAccessDashboards = useSelector( + service, + (state) => + !state.matches( + 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.unauthorized' + ) + ); + + const canUserViewIntegrations = dataStreamSettings?.datasetUserPrivileges?.canViewIntegrations; + + const dataStreamDetails = useSelector(service, (state) => + state.matches('initializing.dataStreamDetails.done') + ? state.context.dataStreamDetails + : undefined + ); + + const { type, dataset, namespace } = indexNameToDataStreamParts(dataStream); + + const datasetDetails: BasicDataStream = { + type, + name: dataset, + namespace, + rawName: dataStream, + }; + + const loadingState = useSelector(service, (state) => ({ + nonAggregatableDatasetLoading: state.matches('initializing.nonAggregatableDataset.fetching'), + dataStreamDetailsLoading: state.matches('initializing.dataStreamDetails.fetching'), + dataStreamSettingsLoading: state.matches('initializing.dataStreamSettings.fetching'), + integrationDetailsLoadings: state.matches( + 'initializing.dataStreamSettings.initializeIntegrations.integrationDetails.fetching' + ), + integrationDetailsLoaded: state.matches( + 'initializing.dataStreamSettings.initializeIntegrations.integrationDetails.done' + ), + integrationDashboardsLoading: state.matches( + 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.fetching' + ), + })); + + const updateTimeRange = useCallback( + ({ start, end, refreshInterval }: OnRefreshProps) => { + service.send({ + type: 'UPDATE_TIME_RANGE', + timeRange: { + from: start, + to: end, + refresh: { ...DEFAULT_DATEPICKER_REFRESH, value: refreshInterval }, + }, + }); + }, + [service] + ); + + return { + service, + fieldFormats, + isIndexNotFoundError, + dataStream, + datasetDetails, + degradedFields, + dataStreamDetails, + breakdownField, + isBreakdownFieldEcs, + isNonAggregatable, + timeRange, + loadingState, + updateTimeRange, + dataStreamSettings, + integrationDetails, + canUserAccessDashboards, + canUserViewIntegrations, + }; +}; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_flyout.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_flyout.tsx index 88bf869a5a2a92..0f9d7981e619ce 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_flyout.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_flyout.tsx @@ -18,7 +18,7 @@ export const useDatasetQualityFlyout = () => { const { dataset: dataStreamStat, - datasetSettings: dataStreamSettings, + dataStreamSettings, datasetDetails: dataStreamDetails, insightsTimeRange, breakdownField, diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_table.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_table.tsx index 347f817f7bf0f3..b205a58dfb98ff 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_table.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_dataset_quality_table.tsx @@ -134,6 +134,7 @@ export const useDatasetQualityTable = () => { showFullDatasetNames, isSizeStatsAvailable, isActiveDataset: isActive, + timeRange, }), [ fieldFormats, @@ -146,6 +147,7 @@ export const useDatasetQualityTable = () => { showFullDatasetNames, isSizeStatsAvailable, isActive, + timeRange, ] ); diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs.ts new file mode 100644 index 00000000000000..7842fe81966f35 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs.ts @@ -0,0 +1,189 @@ +/* + * 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 { useCallback, useState, useMemo, useEffect } from 'react'; +import type { Action } from '@kbn/ui-actions-plugin/public'; +import { fieldSupportsBreakdown } from '@kbn/unified-histogram-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { useEuiTheme } from '@elastic/eui'; +import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; +import { DEFAULT_LOGS_DATA_VIEW } from '../../common/constants'; +import { useCreateDataView } from './use_create_dataview'; +import { useKibanaContextForPlugin } from '../utils'; +import { useDatasetQualityDetailsState } from './use_dataset_quality_details_state'; +import { getLensAttributes } from '../components/dataset_quality_details/overview/document_trends/degraded_docs/lens_attributes'; +import { useDatasetQualityDetailsRedirectLink } from './use_dataset_quality_details_redirect_link'; + +const exploreDataInLogsExplorerText = i18n.translate( + 'xpack.datasetQuality.details.chartExploreDataInLogsExplorerText', + { + defaultMessage: 'Explore data in Logs Explorer', + } +); + +const exploreDataInDiscoverText = i18n.translate( + 'xpack.datasetQuality.details.chartExploreDataInDiscoverText', + { + defaultMessage: 'Explore data in Discover', + } +); + +const openInLensText = i18n.translate('xpack.datasetQuality.details.chartOpenInLensText', { + defaultMessage: 'Open in Lens', +}); + +const ACTION_EXPLORE_IN_LOGS_EXPLORER = 'ACTION_EXPLORE_IN_LOGS_EXPLORER'; +const ACTION_OPEN_IN_LENS = 'ACTION_OPEN_IN_LENS'; + +export const useDegradedDocs = () => { + const { euiTheme } = useEuiTheme(); + const { + services: { lens }, + } = useKibanaContextForPlugin(); + const { service, dataStream, datasetDetails, timeRange, breakdownField, integrationDetails } = + useDatasetQualityDetailsState(); + + const [isChartLoading, setIsChartLoading] = useState(undefined); + const [attributes, setAttributes] = useState | undefined>( + undefined + ); + + const { dataView } = useCreateDataView({ + indexPatternString: getDataViewIndexPattern(dataStream), + }); + + const breakdownDataViewField = useMemo( + () => getDataViewField(dataView, breakdownField), + [breakdownField, dataView] + ); + + const handleChartLoading = (isLoading: boolean) => { + setIsChartLoading(isLoading); + }; + + const handleBreakdownFieldChange = useCallback( + (field: DataViewField | undefined) => { + service.send({ + type: 'BREAKDOWN_FIELD_CHANGE', + breakdownField: field?.name, + }); + }, + [service] + ); + + useEffect(() => { + const dataStreamName = dataStream ?? DEFAULT_LOGS_DATA_VIEW; + const datasetTitle = + integrationDetails?.integration?.datasets?.[datasetDetails.name] ?? datasetDetails.name; + + const lensAttributes = getLensAttributes({ + color: euiTheme.colors.danger, + dataStream: dataStreamName, + datasetTitle, + breakdownFieldName: breakdownDataViewField?.name, + }); + setAttributes(lensAttributes); + }, [ + breakdownDataViewField?.name, + euiTheme.colors.danger, + setAttributes, + dataStream, + integrationDetails?.integration?.datasets, + datasetDetails.name, + ]); + + const openInLensCallback = useCallback(() => { + if (attributes) { + lens.navigateToPrefilledEditor({ + id: '', + timeRange, + attributes, + }); + } + }, [attributes, lens, timeRange]); + + const getOpenInLensAction = useMemo(() => { + return { + id: ACTION_OPEN_IN_LENS, + type: 'link', + order: 17, + getDisplayName(): string { + return openInLensText; + }, + getIconType(): string { + return 'visArea'; + }, + async isCompatible(): Promise { + return true; + }, + async execute(): Promise { + return openInLensCallback(); + }, + }; + }, [openInLensCallback]); + + const redirectLinkProps = useDatasetQualityDetailsRedirectLink({ + dataStreamStat: datasetDetails, + query: { language: 'kuery', query: '_ignored:*' }, + timeRangeConfig: timeRange, + breakdownField: breakdownDataViewField?.name, + }); + + const getOpenInLogsExplorerAction = useMemo(() => { + return { + id: ACTION_EXPLORE_IN_LOGS_EXPLORER, + type: 'link', + getDisplayName(): string { + return redirectLinkProps?.isLogsExplorerAvailable + ? exploreDataInLogsExplorerText + : exploreDataInDiscoverText; + }, + getHref: async () => { + return redirectLinkProps.linkProps.href; + }, + getIconType(): string | undefined { + return 'visTable'; + }, + async isCompatible(): Promise { + return true; + }, + async execute(): Promise { + return redirectLinkProps.navigate(); + }, + order: 18, + }; + }, [redirectLinkProps]); + + const extraActions: Action[] = [getOpenInLensAction, getOpenInLogsExplorerAction]; + + return { + attributes, + dataView, + breakdown: { + dataViewField: breakdownDataViewField, + fieldSupportsBreakdown: breakdownDataViewField + ? fieldSupportsBreakdown(breakdownDataViewField) + : true, + onChange: handleBreakdownFieldChange, + }, + extraActions, + isChartLoading, + onChartLoading: handleChartLoading, + setAttributes, + setIsChartLoading, + }; +}; + +function getDataViewIndexPattern(dataStream: string | undefined) { + return dataStream ?? DEFAULT_LOGS_DATA_VIEW; +} + +function getDataViewField(dataView: DataView | undefined, fieldName: string | undefined) { + return fieldName && dataView + ? dataView.fields.find((field) => field.name === fieldName) + : undefined; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs_chart.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs_chart.tsx index 682f0205c7438c..6840f2a4088a92 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs_chart.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_docs_chart.tsx @@ -14,12 +14,12 @@ import { useEuiTheme } from '@elastic/eui'; import { type DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { useDatasetQualityContext } from '../components/dataset_quality/context'; import { DEFAULT_LOGS_DATA_VIEW } from '../../common/constants'; -import { getLensAttributes } from '../components/flyout/degraded_docs_trend/lens_attributes'; import { useCreateDataView } from './use_create_dataview'; import { useRedirectLink } from './use_redirect_link'; import { useDatasetQualityFlyout } from './use_dataset_quality_flyout'; import { useKibanaContextForPlugin } from '../utils'; import { useDatasetDetailsTelemetry } from './use_telemetry'; +import { getLensAttributes } from '../components/dataset_quality_details/overview/document_trends/degraded_docs/lens_attributes'; const exploreDataInLogsExplorerText = i18n.translate( 'xpack.datasetQuality.flyoutChartExploreDataInLogsExplorerText', 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 new file mode 100644 index 00000000000000..86aed2df771208 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_degraded_fields.ts @@ -0,0 +1,78 @@ +/* + * 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 { useSelector } from '@xstate/react'; +import { useCallback, useMemo } from 'react'; +import { orderBy } from 'lodash'; +import { DegradedField } from '../../common/data_streams_stats'; +import { SortDirection } from '../../common/types'; +import { + DEFAULT_DEGRADED_FIELD_SORT_DIRECTION, + DEFAULT_DEGRADED_FIELD_SORT_FIELD, +} from '../../common/constants'; +import { useKibanaContextForPlugin } from '../utils'; +import { useDatasetQualityDetailsState } from './use_dataset_quality_details_state'; + +export type DegradedFieldSortField = keyof DegradedField; + +export function useDegradedFields() { + const { service } = useDatasetQualityDetailsState(); + const { + services: { fieldFormats }, + } = useKibanaContextForPlugin(); + + const degradedFields = useSelector(service, (state) => state.context.degradedFields) ?? {}; + const { data, table } = degradedFields; + const { page, rowsPerPage, sort } = table; + + const totalItemCount = data?.length ?? 0; + + const pagination = { + pageIndex: page, + pageSize: rowsPerPage, + totalItemCount, + hidePerPageOptions: true, + }; + + const onTableChange = useCallback( + (options: { + page: { index: number; size: number }; + sort?: { field: DegradedFieldSortField; direction: SortDirection }; + }) => { + service.send({ + type: 'UPDATE_DEGRADED_FIELDS_TABLE_CRITERIA', + degraded_field_criteria: { + page: options.page.index, + rowsPerPage: options.page.size, + sort: { + field: options.sort?.field || DEFAULT_DEGRADED_FIELD_SORT_FIELD, + direction: options.sort?.direction || DEFAULT_DEGRADED_FIELD_SORT_DIRECTION, + }, + }, + }); + }, + [service] + ); + + const renderedItems = useMemo(() => { + const sortedItems = orderBy(data, sort.field, sort.direction); + return sortedItems.slice(page * rowsPerPage, (page + 1) * rowsPerPage); + }, [data, sort.field, sort.direction, page, rowsPerPage]); + + const isLoading = useSelector(service, (state) => + state.matches('initializing.dataStreamDegradedFields.fetching') + ); + + return { + isLoading, + pagination, + onTableChange, + renderedItems, + sort: { sort }, + fieldFormats, + totalItemCount, + }; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_integration_actions.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_integration_actions.ts new file mode 100644 index 00000000000000..0fce743875a605 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_integration_actions.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getRouterLinkProps } from '@kbn/router-utils'; +import { useMemo, useCallback } from 'react'; +import useToggle from 'react-use/lib/useToggle'; +import { MANAGEMENT_APP_LOCATOR } from '@kbn/deeplinks-management/constants'; +import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics'; +import { useKibanaContextForPlugin } from '../utils'; +import { Dashboard } from '../../common/api_types'; + +export const useIntegrationActions = () => { + const { + services: { + application: { navigateToUrl }, + http: { basePath }, + share, + }, + } = useKibanaContextForPlugin(); + + const [isOpen, toggleIsOpen] = useToggle(false); + + const dashboardLocator = useMemo( + () => share.url.locators.get(DASHBOARD_APP_LOCATOR), + [share.url.locators] + ); + const indexManagementLocator = useMemo( + () => share.url.locators.get(MANAGEMENT_APP_LOCATOR), + [share.url.locators] + ); + + const handleCloseMenu = useCallback(() => { + toggleIsOpen(); + }, [toggleIsOpen]); + const handleToggleMenu = useCallback(() => { + toggleIsOpen(); + }, [toggleIsOpen]); + + const getIntegrationOverviewLinkProps = useCallback( + (name: string, version: string) => { + const href = basePath.prepend(`/app/integrations/detail/${name}-${version}/overview`); + return getRouterLinkProps({ + href, + onClick: () => { + return navigateToUrl(href); + }, + }); + }, + [basePath, navigateToUrl] + ); + const getIndexManagementLinkProps = useCallback( + (params: { sectionId: string; appId: string }) => + getRouterLinkProps({ + href: indexManagementLocator?.getRedirectUrl(params), + onClick: () => { + return indexManagementLocator?.navigate(params); + }, + }), + [indexManagementLocator] + ); + const getDashboardLinkProps = useCallback( + (dashboard: Dashboard) => + getRouterLinkProps({ + href: dashboardLocator?.getRedirectUrl({ dashboardId: dashboard?.id } || ''), + onClick: () => { + return dashboardLocator?.navigate({ dashboardId: dashboard?.id } || ''); + }, + }), + [dashboardLocator] + ); + + return { + isOpen, + handleCloseMenu, + handleToggleMenu, + getIntegrationOverviewLinkProps, + getIndexManagementLinkProps, + getDashboardLinkProps, + }; +}; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_overview_summary_panel.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_overview_summary_panel.ts new file mode 100644 index 00000000000000..5cdd820c9b4aea --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_overview_summary_panel.ts @@ -0,0 +1,77 @@ +/* + * 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 { useSelector } from '@xstate/react'; +import { formatNumber } from '@elastic/eui'; +import { BYTE_NUMBER_FORMAT, MAX_HOSTS_METRIC_VALUE, NUMBER_FORMAT } from '../../common/constants'; +import { useDatasetQualityDetailsContext } from '../components/dataset_quality_details/context'; + +export const useOverviewSummaryPanel = () => { + const { service } = useDatasetQualityDetailsContext(); + const context = useSelector(service, (state) => state.context) ?? {}; + + const isSummaryPanelLoading = useSelector(service, (state) => + state.matches('initializing.dataStreamDetails.fetching') + ); + + const dataStreamDetails = 'dataStreamDetails' in context ? context.dataStreamDetails : {}; + + const services = dataStreamDetails?.services ?? {}; + const serviceKeys = Object.keys(services); + const totalServicesCount = serviceKeys + .map((key: string) => services[key].length) + .reduce((a, b) => a + b, 0); + + const totalDocsCount = formatNumber(dataStreamDetails?.docsCount ?? 0, NUMBER_FORMAT); + + const sizeInBytesAvailable = dataStreamDetails?.sizeBytes !== null; + const sizeInBytes = formatNumber(dataStreamDetails?.sizeBytes ?? 0, BYTE_NUMBER_FORMAT); + const isUserAllowedToSeeSizeInBytes = dataStreamDetails?.userPrivileges?.canMonitor ?? true; + + const hosts = dataStreamDetails?.hosts ?? {}; + const hostKeys = Object.keys(hosts); + const countOfHosts = hostKeys + .map((key: string) => hosts[key].length) + .reduce( + ({ count, anyHostExceedsMax }, hostCount) => ({ + count: count + hostCount, + anyHostExceedsMax: anyHostExceedsMax || hostCount > MAX_HOSTS_METRIC_VALUE, + }), + { count: 0, anyHostExceedsMax: false } + ); + + const totalHostsCount = formatMetricValueForMax( + countOfHosts.anyHostExceedsMax ? countOfHosts.count + 1 : countOfHosts.count, + countOfHosts.count, + NUMBER_FORMAT + ); + + const totalDegradedDocsCount = formatNumber( + dataStreamDetails?.degradedDocsCount ?? 0, + NUMBER_FORMAT + ); + + return { + totalDocsCount, + sizeInBytesAvailable, + sizeInBytes, + isUserAllowedToSeeSizeInBytes, + totalServicesCount, + totalHostsCount, + isSummaryPanelLoading, + totalDegradedDocsCount, + }; +}; + +/** + * Formats a metric value to show a '+' sign if it's above a max value e.g. 50+ + */ +function formatMetricValueForMax(value: number, max: number, numberFormat: string): string { + const exceedsMax = value > max; + const valueToShow = exceedsMax ? max : value; + return `${formatNumber(valueToShow, numberFormat)}${exceedsMax ? '+' : ''}`; +} diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_redirect_link.ts b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_redirect_link.ts index 4a4c6772c412c2..e4fbf0771ee1f6 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_redirect_link.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_redirect_link.ts @@ -16,11 +16,8 @@ import { getRouterLinkProps } from '@kbn/router-utils'; import { RouterLinkProps } from '@kbn/router-utils/src/get_router_link_props'; import { LocatorPublic } from '@kbn/share-plugin/common'; import { LocatorClient } from '@kbn/shared-ux-prompt-no-data-views-types'; -import { useSelector } from '@xstate/react'; -import { useDatasetQualityContext } from '../components/dataset_quality/context'; -import { TimeRangeConfig } from '../state_machines/dataset_quality_controller'; import { useKibanaContextForPlugin } from '../utils'; -import { BasicDataStream } from '../../common/types'; +import { BasicDataStream, TimeRangeConfig } from '../../common/types'; import { useRedirectLinkTelemetry } from './use_telemetry'; export const useRedirectLink = ({ @@ -32,7 +29,7 @@ export const useRedirectLink = ({ }: { dataStreamStat: T; query?: Query | AggregateQuery; - timeRangeConfig?: TimeRangeConfig; + timeRangeConfig: TimeRangeConfig; breakdownField?: string; telemetry?: Parameters[0]['telemetry']; }) => { @@ -40,9 +37,7 @@ export const useRedirectLink = ({ services: { share }, } = useKibanaContextForPlugin(); - const { service } = useDatasetQualityContext(); - const { timeRange } = useSelector(service, (state) => state.context.filters); - const { from, to } = timeRangeConfig || timeRange; + const { from, to } = timeRangeConfig; const logsExplorerLocator = share.url.locators.get(SINGLE_DATASET_LOCATOR_ID); diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_telemetry.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_telemetry.tsx index f32deb8e3b2c28..c473495c0a661d 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_telemetry.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/hooks/use_telemetry.tsx @@ -22,9 +22,10 @@ import { DatasetNavigatedEbtProps, DatasetEbtProps, } from '../services/telemetry'; -import { FlyoutDataset, TimeRangeConfig } from '../state_machines/dataset_quality_controller'; +import { FlyoutDataset } from '../state_machines/dataset_quality_controller'; import { useDatasetQualityContext } from '../components/dataset_quality/context'; import { useDatasetQualityFilters } from './use_dataset_quality_filters'; +import { TimeRangeConfig } from '../../common/types'; export const useRedirectLinkTelemetry = ({ rawName, diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/plugin.tsx b/x-pack/plugins/observability_solution/dataset_quality/public/plugin.tsx index d24408a85532d8..6cb4c129754128 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/plugin.tsx +++ b/x-pack/plugins/observability_solution/dataset_quality/public/plugin.tsx @@ -8,7 +8,9 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { TelemetryService } from './services/telemetry'; import { createDatasetQuality } from './components/dataset_quality'; -import { createDatasetQualityControllerLazyFactory } from './controller/lazy_create_controller'; +import { createDatasetQualityDetails } from './components/dataset_quality_details'; +import { createDatasetQualityControllerLazyFactory } from './controller/dataset_quality/lazy_create_controller'; +import { createDatasetQualityDetailsControllerLazyFactory } from './controller/dataset_quality_details/lazy_create_controller'; import { DataStreamsStatsService } from './services/data_streams_stats'; import { DataStreamDetailsService } from './services/data_stream_details'; import { @@ -22,6 +24,7 @@ export class DatasetQualityPlugin implements Plugin { private telemetry = new TelemetryService(); + constructor(context: PluginInitializerContext) {} public setup(core: CoreSetup, plugins: DatasetQualitySetupDeps) { @@ -54,6 +57,24 @@ export class DatasetQualityPlugin dataStreamDetailsClient, }); - return { DatasetQuality, createDatasetQualityController }; + const DatasetQualityDetails = createDatasetQualityDetails({ + core, + plugins, + telemetryClient, + }); + + const createDatasetQualityDetailsController = createDatasetQualityDetailsControllerLazyFactory({ + core, + plugins, + dataStreamStatsClient, + dataStreamDetailsClient, + }); + + return { + DatasetQuality, + createDatasetQualityController, + DatasetQualityDetails, + createDatasetQualityDetailsController, + }; } } 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 a5813b3190351e..55d21286d7f602 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 @@ -25,13 +25,12 @@ import { GetDataStreamDetailsResponse, GetDataStreamSettingsParams, GetDataStreamSettingsResponse, - GetDataStreamsStatsError, GetIntegrationDashboardsParams, } from '../../../common/data_streams_stats'; import { IDataStreamDetailsClient } from './types'; -import { GetDataStreamsDetailsError } from '../../../common/data_stream_details'; import { Integration } from '../../../common/data_streams_stats/integration'; import { GetDataStreamIntegrationParams } from '../../../common/data_stream_details/types'; +import { DatasetQualityError } from '../../../common/errors'; export class DataStreamDetailsClient implements IDataStreamDetailsClient { constructor(private readonly http: HttpStart) {} @@ -42,16 +41,13 @@ export class DataStreamDetailsClient implements IDataStreamDetailsClient { `/internal/dataset_quality/data_streams/${dataStream}/settings` ) .catch((error) => { - throw new GetDataStreamsStatsError( - `Failed to fetch data stream settings": ${error}`, - error.body.statusCode - ); + throw new DatasetQualityError(`Failed to fetch data stream settings": ${error}`, error); }); const dataStreamSettings = decodeOrThrow( getDataStreamsSettingsResponseRt, (message: string) => - new GetDataStreamsStatsError(`Failed to decode data stream settings response: ${message}"`) + new DatasetQualityError(`Failed to decode data stream settings response: ${message}"`) )(response); return dataStreamSettings as DataStreamSettings; @@ -66,16 +62,13 @@ export class DataStreamDetailsClient implements IDataStreamDetailsClient { } ) .catch((error) => { - throw new GetDataStreamsStatsError( - `Failed to fetch data stream details": ${error}`, - error.body.statusCode - ); + throw new DatasetQualityError(`Failed to fetch data stream details": ${error}`, error); }); const dataStreamDetails = decodeOrThrow( getDataStreamsDetailsResponseRt, (message: string) => - new GetDataStreamsStatsError(`Failed to decode data stream details response: ${message}"`) + new DatasetQualityError(`Failed to decode data stream details response: ${message}"`) )(response); return dataStreamDetails as DataStreamDetails; @@ -94,16 +87,16 @@ export class DataStreamDetailsClient implements IDataStreamDetailsClient { } ) .catch((error) => { - throw new GetDataStreamsDetailsError( + throw new DatasetQualityError( `Failed to fetch data stream degraded fields": ${error}`, - error.body.statusCode + error ); }); return decodeOrThrow( getDataStreamDegradedFieldsResponseRt, (message: string) => - new GetDataStreamsDetailsError( + new DatasetQualityError( `Failed to decode data stream degraded fields response: ${message}"` ) )(response); @@ -115,18 +108,13 @@ export class DataStreamDetailsClient implements IDataStreamDetailsClient { `/internal/dataset_quality/integrations/${integration}/dashboards` ) .catch((error) => { - throw new GetDataStreamsStatsError( - `Failed to fetch integration dashboards": ${error}`, - error.body.statusCode - ); + throw new DatasetQualityError(`Failed to fetch integration dashboards": ${error}`, error); }); const { dashboards } = decodeOrThrow( integrationDashboardsRT, (message: string) => - new GetDataStreamsStatsError( - `Failed to decode integration dashboards response: ${message}"` - ) + new DatasetQualityError(`Failed to decode integration dashboards response: ${message}"`) )(response); return dashboards; @@ -141,16 +129,13 @@ export class DataStreamDetailsClient implements IDataStreamDetailsClient { query: { type }, }) .catch((error) => { - throw new GetDataStreamsStatsError( - `Failed to fetch integrations: ${error}`, - error.body.statusCode - ); + throw new DatasetQualityError(`Failed to fetch integrations: ${error}`, error); }); const { integrations } = decodeOrThrow( getIntegrationsResponseRt, (message: string) => - new GetDataStreamsStatsError(`Failed to decode integrations response: ${message}`) + new DatasetQualityError(`Failed to decode integrations response: ${message}`) )(response); const integration = integrations.find((i) => i.name === integrationName); diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts index bbce86404e1ddb..35cd67a1724f99 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts @@ -13,21 +13,21 @@ import { getIntegrationsResponseRt, getNonAggregatableDatasetsRt, IntegrationResponse, + NonAggregatableDatasets, } from '../../../common/api_types'; import { DEFAULT_DATASET_TYPE } from '../../../common/constants'; import { DataStreamStatServiceResponse, GetDataStreamsDegradedDocsStatsQuery, GetDataStreamsDegradedDocsStatsResponse, - GetDataStreamsStatsError, GetDataStreamsStatsQuery, GetDataStreamsStatsResponse, GetIntegrationsParams, GetNonAggregatableDataStreamsParams, - GetNonAggregatableDataStreamsResponse, } from '../../../common/data_streams_stats'; import { Integration } from '../../../common/data_streams_stats/integration'; import { IDataStreamsStatsClient } from './types'; +import { DatasetQualityError } from '../../../common/errors'; export class DataStreamsStatsClient implements IDataStreamsStatsClient { constructor(private readonly http: HttpStart) {} @@ -40,16 +40,13 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { query: params, }) .catch((error) => { - throw new GetDataStreamsStatsError( - `Failed to fetch data streams stats: ${error}`, - error.body.statusCode - ); + throw new DatasetQualityError(`Failed to fetch data streams stats: ${error}`, error); }); const { dataStreamsStats, datasetUserPrivileges } = decodeOrThrow( getDataStreamsStatsResponseRt, (message: string) => - new GetDataStreamsStatsError(`Failed to decode data streams stats response: ${message}`) + new DatasetQualityError(`Failed to decode data streams stats response: ${message}`) )(response); return { dataStreamsStats, datasetUserPrivileges }; @@ -67,16 +64,16 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { } ) .catch((error) => { - throw new GetDataStreamsStatsError( + throw new DatasetQualityError( `Failed to fetch data streams degraded stats: ${error}`, - error.body.statusCode + error ); }); const { degradedDocs } = decodeOrThrow( getDataStreamsDegradedDocsStatsResponseRt, (message: string) => - new GetDataStreamsStatsError( + new DatasetQualityError( `Failed to decode data streams degraded docs stats response: ${message}` ) )(response); @@ -86,26 +83,20 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { public async getNonAggregatableDatasets(params: GetNonAggregatableDataStreamsParams) { const response = await this.http - .get( - '/internal/dataset_quality/data_streams/non_aggregatable', - { - query: { - ...params, - type: DEFAULT_DATASET_TYPE, - }, - } - ) + .get('/internal/dataset_quality/data_streams/non_aggregatable', { + query: { + ...params, + type: DEFAULT_DATASET_TYPE, + }, + }) .catch((error) => { - throw new GetDataStreamsStatsError( - `Failed to fetch non aggregatable datasets: ${error}`, - error.body.statusCode - ); + throw new DatasetQualityError(`Failed to fetch non aggregatable datasets: ${error}`, error); }); const nonAggregatableDatasets = decodeOrThrow( getNonAggregatableDatasetsRt, (message: string) => - new GetDataStreamsStatsError(`Failed to fetch non aggregatable datasets: ${message}`) + new DatasetQualityError(`Failed to fetch non aggregatable datasets: ${message}`) )(response); return nonAggregatableDatasets; @@ -119,16 +110,13 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { query: params, }) .catch((error) => { - throw new GetDataStreamsStatsError( - `Failed to fetch integrations: ${error}`, - error.body.statusCode - ); + throw new DatasetQualityError(`Failed to fetch integrations: ${error}`, error); }); const { integrations } = decodeOrThrow( getIntegrationsResponseRt, (message: string) => - new GetDataStreamsStatsError(`Failed to decode integrations response: ${message}`) + new DatasetQualityError(`Failed to decode integrations response: ${message}`) )(response); return integrations.map(Integration.create); diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/types.ts b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/types.ts index ba1f9077f45b04..785cdb4a88ccce 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/services/data_streams_stats/types.ts @@ -13,9 +13,9 @@ import { GetDataStreamsStatsQuery, GetIntegrationsParams, GetNonAggregatableDataStreamsParams, - GetNonAggregatableDataStreamsResponse, } from '../../../common/data_streams_stats'; import { Integration } from '../../../common/data_streams_stats/integration'; +import { NonAggregatableDatasets } from '../../../common/api_types'; export type DataStreamsStatsServiceSetup = void; @@ -35,5 +35,5 @@ export interface IDataStreamsStatsClient { getIntegrations(params: GetIntegrationsParams['query']): Promise; getNonAggregatableDatasets( params: GetNonAggregatableDataStreamsParams - ): Promise; + ): Promise; } diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/common/notifications.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/common/notifications.ts new file mode 100644 index 00000000000000..9ec9f71e122cf5 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/common/notifications.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IToasts } from '@kbn/core-notifications-browser'; +import { i18n } from '@kbn/i18n'; + +export const fetchNonAggregatableDatasetsFailedNotifier = (toasts: IToasts, error: Error) => { + toasts.addDanger({ + title: i18n.translate('xpack.datasetQuality.fetchNonAggregatableDatasetsFailed', { + defaultMessage: "We couldn't get non aggregatable datasets information.", + }), + text: error.message, + }); +}; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts index fe83caafce2507..f10ea32da3af6a 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts @@ -26,15 +26,6 @@ export const fetchDatasetDetailsFailedNotifier = (toasts: IToasts, error: Error) }); }; -export const fetchDatasetSettingsFailedNotifier = (toasts: IToasts, error: Error) => { - toasts.addDanger({ - title: i18n.translate('xpack.datasetQuality.fetchDatasetSettingsFailed', { - defaultMessage: "Data set settings couldn't be loaded.", - }), - text: error.message, - }); -}; - export const fetchDegradedStatsFailedNotifier = (toasts: IToasts, error: Error) => { toasts.addDanger({ title: i18n.translate('xpack.datasetQuality.fetchDegradedStatsFailed', { @@ -53,15 +44,6 @@ export const fetchNonAggregatableDatasetsFailedNotifier = (toasts: IToasts, erro }); }; -export const fetchIntegrationDashboardsFailedNotifier = (toasts: IToasts, error: Error) => { - toasts.addDanger({ - title: i18n.translate('xpack.datasetQuality.fetchIntegrationDashboardsFailed', { - defaultMessage: "We couldn't get your integration dashboards.", - }), - text: error.message, - }); -}; - export const fetchIntegrationsFailedNotifier = (toasts: IToasts, error: Error) => { toasts.addDanger({ title: i18n.translate('xpack.datasetQuality.fetchIntegrationsFailed', { @@ -71,22 +53,6 @@ export const fetchIntegrationsFailedNotifier = (toasts: IToasts, error: Error) = }); }; -export const fetchDataStreamIntegrationFailedNotifier = ( - toasts: IToasts, - error: Error, - integrationName?: string -) => { - toasts.addDanger({ - title: i18n.translate('xpack.datasetQuality.flyout.fetchIntegrationsFailed', { - defaultMessage: "We couldn't get {integrationName} integration info.", - values: { - integrationName, - }, - }), - text: error.message, - }); -}; - export const noDatasetSelected = i18n.translate( 'xpack.datasetQuality.fetchDatasetDetailsFailed.noDatasetSelected', { @@ -96,7 +62,7 @@ export const noDatasetSelected = i18n.translate( export const assertBreakdownFieldEcsFailedNotifier = (toasts: IToasts, error: Error) => { toasts.addDanger({ - title: i18n.translate('xpack.datasetQuality. assertBreakdownFieldEcsFailed', { + title: i18n.translate('xpack.datasetQuality.assertBreakdownFieldEcsFailed', { defaultMessage: "We couldn't retrieve breakdown field metadata.", }), text: error.message, diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts index e2a6eda4d0e6e4..fb6c03fae153af 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts @@ -9,7 +9,12 @@ import { IToasts } from '@kbn/core/public'; import { getDateISORange } from '@kbn/timerange'; import { assign, createMachine, DoneInvokeEvent, InterpreterFrom } from 'xstate'; import { DatasetQualityStartDeps } from '../../../types'; -import { Dashboard, DataStreamStat, DegradedFieldResponse } from '../../../../common/api_types'; +import { + Dashboard, + DataStreamStat, + DegradedFieldResponse, + NonAggregatableDatasets, +} from '../../../../common/api_types'; import { Integration } from '../../../../common/data_streams_stats/integration'; import { IDataStreamDetailsClient } from '../../../services/data_stream_details'; import { @@ -18,7 +23,6 @@ import { GetDataStreamsStatsQuery, GetIntegrationsParams, GetNonAggregatableDataStreamsParams, - GetNonAggregatableDataStreamsResponse, DataStreamStatServiceResponse, } from '../../../../common/data_streams_stats'; import { DegradedDocsStat } from '../../../../common/data_streams_stats/malformed_docs_stat'; @@ -28,17 +32,14 @@ import { IDataStreamsStatsClient } from '../../../services/data_streams_stats'; import { generateDatasets } from '../../../utils'; import { DEFAULT_CONTEXT } from './defaults'; import { - fetchDatasetSettingsFailedNotifier, fetchDatasetDetailsFailedNotifier, fetchDatasetStatsFailedNotifier, fetchDegradedStatsFailedNotifier, - fetchIntegrationDashboardsFailedNotifier, fetchIntegrationsFailedNotifier, noDatasetSelected, - fetchNonAggregatableDatasetsFailedNotifier, - fetchDataStreamIntegrationFailedNotifier, assertBreakdownFieldEcsFailedNotifier, } from './notifications'; +import { fetchNonAggregatableDatasetsFailedNotifier } from '../../common/notifications'; import { DatasetQualityControllerContext, DatasetQualityControllerEvent, @@ -46,6 +47,11 @@ import { DefaultDatasetQualityControllerState, FlyoutDataset, } from './types'; +import { + fetchDataStreamSettingsFailedNotifier, + fetchIntegrationDashboardsFailedNotifier, + fetchDataStreamIntegrationFailedNotifier, +} from '../../dataset_quality_details_controller/notifications'; export const createPureDatasetQualityControllerStateMachine = ( initialContext: DatasetQualityControllerContext @@ -269,7 +275,7 @@ export const createPureDatasetQualityControllerStateMachine = ( }, onError: { target: 'done', - actions: ['notifyFetchDatasetSettingsFailed'], + actions: ['notifyFetchDataStreamSettingsFailed'], }, }, }, @@ -628,7 +634,7 @@ export const createPureDatasetQualityControllerStateMachine = ( storeNonAggregatableDatasets: assign( ( _context: DefaultDatasetQualityControllerState, - event: DoneInvokeEvent + event: DoneInvokeEvent ) => { return 'data' in event ? { @@ -642,7 +648,7 @@ export const createPureDatasetQualityControllerStateMachine = ( ? { flyout: { ...context.flyout, - datasetSettings: (event.data ?? {}) as DataStreamSettings, + dataStreamSettings: (event.data ?? {}) as DataStreamSettings, }, } : {}; @@ -660,7 +666,7 @@ export const createPureDatasetQualityControllerStateMachine = ( storeDatasetIsNonAggregatable: assign( ( context: DefaultDatasetQualityControllerState, - event: DoneInvokeEvent + event: DoneInvokeEvent ) => { return 'data' in event ? { @@ -758,8 +764,8 @@ export const createDatasetQualityControllerStateMachine = ({ fetchDegradedStatsFailedNotifier(toasts, event.data), notifyFetchNonAggregatableDatasetsFailed: (_context, event: DoneInvokeEvent) => fetchNonAggregatableDatasetsFailedNotifier(toasts, event.data), - notifyFetchDatasetSettingsFailed: (_context, event: DoneInvokeEvent) => - fetchDatasetSettingsFailedNotifier(toasts, event.data), + notifyFetchDataStreamSettingsFailed: (_context, event: DoneInvokeEvent) => + fetchDataStreamSettingsFailedNotifier(toasts, event.data), notifyFetchDatasetDetailsFailed: (_context, event: DoneInvokeEvent) => fetchDatasetDetailsFailedNotifier(toasts, event.data), notifyFetchIntegrationDashboardsFailed: (_context, event: DoneInvokeEvent) => @@ -767,7 +773,7 @@ export const createDatasetQualityControllerStateMachine = ({ notifyFetchIntegrationsFailed: (_context, event: DoneInvokeEvent) => fetchIntegrationsFailedNotifier(toasts, event.data), notifyFetchDatasetIntegrationsFailed: (context, event: DoneInvokeEvent) => { - const integrationName = context.flyout.datasetSettings?.integration; + const integrationName = context.flyout.dataStreamSettings?.integration; return fetchDataStreamIntegrationFailedNotifier(toasts, event.data, integrationName); }, notifyAssertBreakdownFieldEcsFailed: (_context, event: DoneInvokeEvent) => @@ -826,7 +832,7 @@ export const createDatasetQualityControllerStateMachine = ({ }, loadDataStreamSettings: (context) => { if (!context.flyout.dataset) { - fetchDatasetSettingsFailedNotifier(toasts, new Error(noDatasetSelected)); + fetchDataStreamSettingsFailedNotifier(toasts, new Error(noDatasetSelected)); return Promise.resolve({}); } @@ -842,11 +848,11 @@ export const createDatasetQualityControllerStateMachine = ({ }); }, loadDataStreamIntegration: (context) => { - if (context.flyout.datasetSettings?.integration && context.flyout.dataset) { + if (context.flyout.dataStreamSettings?.integration && context.flyout.dataset) { const { type } = context.flyout.dataset; return dataStreamDetailsClient.getDataStreamIntegration({ type: type as DataStreamType, - integrationName: context.flyout.datasetSettings.integration, + integrationName: context.flyout.dataStreamSettings.integration, }); } return Promise.resolve(); @@ -874,9 +880,9 @@ export const createDatasetQualityControllerStateMachine = ({ }); }, loadIntegrationDashboards: (context) => { - if (context.flyout.datasetSettings?.integration) { + if (context.flyout.dataStreamSettings?.integration) { return dataStreamDetailsClient.getIntegrationDashboards({ - integration: context.flyout.datasetSettings.integration, + integration: context.flyout.dataStreamSettings.integration, }); } @@ -932,7 +938,3 @@ export const createDatasetQualityControllerStateMachine = ({ export type DatasetQualityControllerStateService = InterpreterFrom< typeof createDatasetQualityControllerStateMachine >; - -export type DatasetQualityControllerStateMachine = ReturnType< - typeof createDatasetQualityControllerStateMachine ->; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts index c258962f56b8d7..d6bfaaad216b98 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts @@ -6,9 +6,12 @@ */ import { DoneInvokeEvent } from 'xstate'; -import { RefreshInterval, TimeRange } from '@kbn/data-plugin/common'; -import { QualityIndicators, SortDirection } from '../../../../common/types'; -import { Dashboard, DatasetUserPrivileges } from '../../../../common/api_types'; +import { QualityIndicators, TableCriteria, TimeRangeConfig } from '../../../../common/types'; +import { + Dashboard, + DatasetUserPrivileges, + NonAggregatableDatasets, +} from '../../../../common/api_types'; import { Integration } from '../../../../common/data_streams_stats/integration'; import { DatasetTableSortField, DegradedFieldSortField } from '../../../hooks'; import { DegradedDocsStat } from '../../../../common/data_streams_stats/malformed_docs_stat'; @@ -19,7 +22,6 @@ import { DataStreamStatServiceResponse, DataStreamStat, DataStreamStatType, - GetNonAggregatableDataStreamsResponse, DegradedField, DegradedFieldResponse, } from '../../../../common/data_streams_stats'; @@ -29,24 +31,11 @@ export type FlyoutDataset = Omit< 'type' | 'size' | 'sizeBytes' | 'lastActivity' | 'degradedDocs' > & { type: string }; -interface TableCriteria { - page: number; - rowsPerPage: number; - sort: { - field: TSortField; - direction: SortDirection; - }; -} - export interface DegradedFields { table: TableCriteria; data?: DegradedField[]; } -export type TimeRangeConfig = Pick & { - refresh: RefreshInterval; -}; - interface FiltersCriteria { inactive: boolean; fullNames: boolean; @@ -69,7 +58,7 @@ export interface WithTableOptions { export interface WithFlyoutOptions { flyout: { dataset?: FlyoutDataset; - datasetSettings?: DataStreamSettings; + dataStreamSettings?: DataStreamSettings; datasetDetails?: DataStreamDetails; insightsTimeRange?: TimeRangeConfig; breakdownField?: string; @@ -135,10 +124,6 @@ export type DatasetQualityControllerTypeState = value: 'degradedDocs.fetching'; context: DefaultDatasetQualityStateContext; } - | { - value: 'datasets.loaded'; - context: DefaultDatasetQualityStateContext; - } | { value: 'integrations.fetching'; context: DefaultDatasetQualityStateContext; @@ -250,7 +235,7 @@ export type DatasetQualityControllerEvent = query: string; } | DoneInvokeEvent - | DoneInvokeEvent + | DoneInvokeEvent | DoneInvokeEvent | DoneInvokeEvent | DoneInvokeEvent 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 new file mode 100644 index 00000000000000..024e49a9b83f49 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/defaults.ts @@ -0,0 +1,32 @@ +/* + * 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 { + DEFAULT_DATEPICKER_REFRESH, + DEFAULT_DEGRADED_FIELD_SORT_DIRECTION, + DEFAULT_DEGRADED_FIELD_SORT_FIELD, + DEFAULT_TIME_RANGE, +} from '../../../common/constants'; +import { DefaultDatasetQualityDetailsContext } from './types'; + +export const DEFAULT_CONTEXT: DefaultDatasetQualityDetailsContext = { + degradedFields: { + table: { + page: 0, + rowsPerPage: 10, + sort: { + field: DEFAULT_DEGRADED_FIELD_SORT_FIELD, + direction: DEFAULT_DEGRADED_FIELD_SORT_DIRECTION, + }, + }, + }, + isIndexNotFoundError: false, + timeRange: { + ...DEFAULT_TIME_RANGE, + refresh: DEFAULT_DATEPICKER_REFRESH, + }, +}; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/index.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/index.ts new file mode 100644 index 00000000000000..069d40a3e8cddd --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/index.ts @@ -0,0 +1,11 @@ +/* + * 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 './state_machine'; +export * from './types'; +export * from './defaults'; +export * from './state_machine'; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/notifications.ts b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/notifications.ts new file mode 100644 index 00000000000000..b501fd02bdcf3f --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/notifications.ts @@ -0,0 +1,61 @@ +/* + * 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 { IToasts } from '@kbn/core-notifications-browser'; +import { i18n } from '@kbn/i18n'; + +export const fetchDataStreamDetailsFailedNotifier = (toasts: IToasts, error: Error) => { + toasts.addDanger({ + title: i18n.translate('xpack.datasetQuality.details.fetchDataStreamDetailsFailed', { + defaultMessage: "We couldn't get your datastream details.", + }), + text: error.message, + }); +}; + +export const assertBreakdownFieldEcsFailedNotifier = (toasts: IToasts, error: Error) => { + toasts.addDanger({ + title: i18n.translate('xpack.datasetQuality.details.checkBreakdownFieldEcsFailed', { + defaultMessage: "We couldn't retrieve breakdown field metadata.", + }), + text: error.message, + }); +}; + +export const fetchDataStreamSettingsFailedNotifier = (toasts: IToasts, error: Error) => { + toasts.addDanger({ + title: i18n.translate('xpack.datasetQuality.details.fetchDataStreamSettingsFailed', { + defaultMessage: "Data stream settings couldn't be loaded.", + }), + text: error.message, + }); +}; + +export const fetchIntegrationDashboardsFailedNotifier = (toasts: IToasts, error: Error) => { + toasts.addDanger({ + title: i18n.translate('xpack.datasetQuality.details.fetchIntegrationDashboardsFailed', { + defaultMessage: "We couldn't get your integration dashboards.", + }), + text: error.message, + }); +}; + +export const fetchDataStreamIntegrationFailedNotifier = ( + toasts: IToasts, + error: Error, + integrationName?: string +) => { + toasts.addDanger({ + title: i18n.translate('xpack.datasetQuality.details.fetchIntegrationsFailed', { + defaultMessage: "We couldn't get {integrationName} integration info.", + values: { + integrationName, + }, + }), + text: error.message, + }); +}; 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 new file mode 100644 index 00000000000000..74dca949901904 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/state_machine.ts @@ -0,0 +1,504 @@ +/* + * 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 { assign, createMachine, DoneInvokeEvent, InterpreterFrom } from 'xstate'; +import { getDateISORange } from '@kbn/timerange'; +import type { IToasts } from '@kbn/core-notifications-browser'; +import { + DatasetQualityDetailsControllerContext, + DatasetQualityDetailsControllerEvent, + DatasetQualityDetailsControllerTypeState, +} from './types'; +import { DatasetQualityStartDeps } from '../../types'; +import { IDataStreamsStatsClient } from '../../services/data_streams_stats'; +import { IDataStreamDetailsClient } from '../../services/data_stream_details'; +import { indexNameToDataStreamParts } from '../../../common/utils'; +import { + Dashboard, + DataStreamDetails, + DataStreamSettings, + DegradedFieldResponse, + NonAggregatableDatasets, +} from '../../../common/api_types'; +import { fetchNonAggregatableDatasetsFailedNotifier } from '../common/notifications'; +import { + fetchDataStreamDetailsFailedNotifier, + assertBreakdownFieldEcsFailedNotifier, + fetchDataStreamSettingsFailedNotifier, + fetchDataStreamIntegrationFailedNotifier, + fetchIntegrationDashboardsFailedNotifier, +} from './notifications'; +import { Integration } from '../../../common/data_streams_stats/integration'; + +export const createPureDatasetQualityDetailsControllerStateMachine = ( + initialContext: DatasetQualityDetailsControllerContext +) => + createMachine< + DatasetQualityDetailsControllerContext, + DatasetQualityDetailsControllerEvent, + DatasetQualityDetailsControllerTypeState + >( + { + id: 'DatasetQualityDetailsController', + context: initialContext, + predictableActionArguments: true, + initial: 'uninitialized', + states: { + uninitialized: { + always: { + target: 'initializing', + }, + }, + initializing: { + type: 'parallel', + states: { + nonAggregatableDataset: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'checkDatasetIsAggregatable', + onDone: { + target: 'done', + actions: ['storeDatasetAggregatableStatus'], + }, + onError: [ + { + target: '#DatasetQualityDetailsController.indexNotFound', + cond: 'isIndexNotFoundError', + }, + { + target: 'done', + actions: ['notifyFailedFetchForAggregatableDatasets'], + }, + ], + }, + }, + done: { + on: { + UPDATE_TIME_RANGE: { + target: 'fetching', + actions: ['storeTimeRange'], + }, + }, + }, + }, + }, + dataStreamDetails: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'loadDataStreamDetails', + onDone: { + target: 'done', + actions: ['storeDataStreamDetails'], + }, + onError: [ + { + target: '#DatasetQualityDetailsController.indexNotFound', + cond: 'isIndexNotFoundError', + }, + { + target: 'done', + actions: ['notifyFetchDataStreamDetailsFailed'], + }, + ], + }, + }, + done: { + on: { + UPDATE_TIME_RANGE: { + target: 'fetching', + actions: ['storeTimeRange'], + }, + BREAKDOWN_FIELD_CHANGE: { + target: + '#DatasetQualityDetailsController.initializing.checkBreakdownFieldIsEcs.fetching', + actions: ['storeBreakDownField'], + }, + }, + }, + }, + }, + checkBreakdownFieldIsEcs: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'checkBreakdownFieldIsEcs', + onDone: { + target: 'done', + actions: ['storeBreakdownFieldEcsStatus'], + }, + onError: { + target: 'done', + actions: ['notifyCheckBreakdownFieldIsEcsFailed'], + }, + }, + }, + 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'], + }, + }, + }, + }, + }, + dataStreamSettings: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'loadDataStreamSettings', + onDone: { + target: 'initializeIntegrations', + actions: ['storeDataStreamSettings'], + }, + onError: [ + { + target: '#DatasetQualityDetailsController.indexNotFound', + cond: 'isIndexNotFoundError', + }, + { + target: 'done', + actions: ['notifyFetchDataStreamSettingsFailed'], + }, + ], + }, + }, + initializeIntegrations: { + type: 'parallel', + states: { + integrationDetails: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'loadDataStreamIntegration', + onDone: { + target: 'done', + actions: ['storeDataStreamIntegration'], + }, + onError: { + target: 'done', + actions: ['notifyFetchDatasetIntegrationsFailed'], + }, + }, + }, + done: { + type: 'final', + }, + }, + }, + integrationDashboards: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'loadIntegrationDashboards', + onDone: { + target: 'done', + actions: ['storeIntegrationDashboards'], + }, + onError: [ + { + target: 'unauthorized', + cond: 'checkIfActionForbidden', + }, + { + target: 'done', + actions: ['notifyFetchIntegrationDashboardsFailed'], + }, + ], + }, + }, + done: { + type: 'final', + }, + unauthorized: { + type: 'final', + }, + }, + }, + }, + }, + done: { + on: { + UPDATE_TIME_RANGE: { + target: 'fetching', + actions: ['resetDegradedFieldPageAndRowsPerPage'], + }, + }, + }, + }, + }, + }, + }, + indexNotFound: { + entry: 'handleIndexNotFoundError', + }, + }, + }, + { + actions: { + storeDatasetAggregatableStatus: assign( + (_context, event: DoneInvokeEvent) => { + return 'data' in event + ? { + isNonAggregatable: !event.data.aggregatable, + } + : {}; + } + ), + storeTimeRange: assign((context, event) => { + return { + timeRange: 'timeRange' in event ? event.timeRange : context.timeRange, + }; + }), + storeDataStreamDetails: assign((_context, event: DoneInvokeEvent) => { + return 'data' in event + ? { + dataStreamDetails: event.data, + } + : {}; + }), + storeBreakDownField: assign((_context, event) => { + return 'breakdownField' in event ? { breakdownField: event.breakdownField } : {}; + }), + storeBreakdownFieldEcsStatus: assign((_context, event: DoneInvokeEvent) => { + return 'data' in event + ? { + isBreakdownFieldEcs: event.data, + } + : {}; + }), + storeDegradedFields: assign((context, event: DoneInvokeEvent) => { + return 'data' in event + ? { + degradedFields: { + ...context.degradedFields, + data: event.data.degradedFields, + }, + } + : {}; + }), + storeDegradedFieldTableOptions: assign((context, event) => { + return 'degraded_field_criteria' in event + ? { + degradedFields: { + ...context.degradedFields, + table: event.degraded_field_criteria, + }, + } + : {}; + }), + resetDegradedFieldPageAndRowsPerPage: assign((context, _event) => ({ + degradedFields: { + ...context.degradedFields, + table: { + ...context.degradedFields.table, + page: 0, + rowsPerPage: 10, + }, + }, + })), + storeDataStreamSettings: assign((_context, event: DoneInvokeEvent) => { + return 'data' in event + ? { + dataStreamSettings: event.data, + } + : {}; + }), + storeDataStreamIntegration: assign((context, event: DoneInvokeEvent) => { + return 'data' in event + ? { + integration: event.data, + } + : {}; + }), + storeIntegrationDashboards: assign((context, event: DoneInvokeEvent) => { + return 'data' in event + ? { + integrationDashboards: event.data, + } + : {}; + }), + handleIndexNotFoundError: assign(() => { + return { + isIndexNotFoundError: true, + }; + }), + }, + guards: { + checkIfActionForbidden: (context, event) => { + return ( + 'data' in event && + typeof event.data === 'object' && + 'statusCode' in event.data! && + event.data.statusCode === 403 + ); + }, + isIndexNotFoundError: (_, event) => { + return ( + ('data' in event && + typeof event.data === 'object' && + 'statusCode' in event.data && + event.data.statusCode === 500 && + 'originalMessage' in event.data && + (event.data.originalMessage as string)?.includes('index_not_found_exception')) ?? + false + ); + }, + }, + } + ); + +export interface DatasetQualityDetailsControllerStateMachineDependencies { + initialContext: DatasetQualityDetailsControllerContext; + plugins: DatasetQualityStartDeps; + toasts: IToasts; + dataStreamStatsClient: IDataStreamsStatsClient; + dataStreamDetailsClient: IDataStreamDetailsClient; +} + +export const createDatasetQualityDetailsControllerStateMachine = ({ + initialContext, + plugins, + toasts, + dataStreamStatsClient, + dataStreamDetailsClient, +}: DatasetQualityDetailsControllerStateMachineDependencies) => + createPureDatasetQualityDetailsControllerStateMachine(initialContext).withConfig({ + actions: { + notifyFailedFetchForAggregatableDatasets: (_context, event: DoneInvokeEvent) => + fetchNonAggregatableDatasetsFailedNotifier(toasts, event.data), + notifyFetchDataStreamDetailsFailed: (_context, event: DoneInvokeEvent) => + fetchDataStreamDetailsFailedNotifier(toasts, event.data), + notifyCheckBreakdownFieldIsEcsFailed: (_context, event: DoneInvokeEvent) => + assertBreakdownFieldEcsFailedNotifier(toasts, event.data), + notifyFetchDataStreamSettingsFailed: (_context, event: DoneInvokeEvent) => + fetchDataStreamSettingsFailedNotifier(toasts, event.data), + notifyFetchIntegrationDashboardsFailed: (_context, event: DoneInvokeEvent) => + fetchIntegrationDashboardsFailedNotifier(toasts, event.data), + notifyFetchDatasetIntegrationsFailed: (context, event: DoneInvokeEvent) => { + const integrationName = + 'dataStreamSettings' in context ? context.dataStreamSettings?.integration : undefined; + return fetchDataStreamIntegrationFailedNotifier(toasts, event.data, integrationName); + }, + }, + services: { + checkDatasetIsAggregatable: (context) => { + const { type } = indexNameToDataStreamParts(context.dataStream); + const { startDate: start, endDate: end } = getDateISORange(context.timeRange); + + return dataStreamStatsClient.getNonAggregatableDatasets({ + type, + start, + end, + dataStream: context.dataStream, + }); + }, + loadDataStreamDetails: (context) => { + const { startDate: start, endDate: end } = getDateISORange(context.timeRange); + + return dataStreamDetailsClient.getDataStreamDetails({ + dataStream: context.dataStream, + start, + end, + }); + }, + checkBreakdownFieldIsEcs: async (context) => { + if (context.breakdownField) { + const allowedFieldSources = ['ecs', 'metadata']; + + // This timeout is to avoid a runtime error that randomly happens on breakdown field change + // TypeError: Cannot read properties of undefined (reading 'timeFieldName') + await new Promise((res) => setTimeout(res, 300)); + + const client = await plugins.fieldsMetadata.getClient(); + const { fields } = await client.find({ + attributes: ['source'], + fieldNames: [context.breakdownField], + }); + + const breakdownFieldSource = fields[context.breakdownField]?.source; + + return !!(breakdownFieldSource && allowedFieldSources.includes(breakdownFieldSource)); + } + + return false; + }, + loadDegradedFields: (context) => { + const { startDate: start, endDate: end } = getDateISORange(context.timeRange); + + return dataStreamDetailsClient.getDataStreamDegradedFields({ + dataStream: context.dataStream, + start, + end, + }); + }, + loadDataStreamSettings: (context) => { + return dataStreamDetailsClient.getDataStreamSettings({ + dataStream: context.dataStream, + }); + }, + loadDataStreamIntegration: (context) => { + if ('dataStreamSettings' in context && context.dataStreamSettings?.integration) { + const { type } = indexNameToDataStreamParts(context.dataStream); + return dataStreamDetailsClient.getDataStreamIntegration({ + type, + integrationName: context.dataStreamSettings.integration, + }); + } + return Promise.resolve(); + }, + loadIntegrationDashboards: (context) => { + if ('dataStreamSettings' in context && context.dataStreamSettings?.integration) { + return dataStreamDetailsClient.getIntegrationDashboards({ + integration: context.dataStreamSettings.integration, + }); + } + + return Promise.resolve(); + }, + }, + }); + +export type DatasetQualityDetailsControllerStateService = InterpreterFrom< + typeof createDatasetQualityDetailsControllerStateMachine +>; 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 new file mode 100644 index 00000000000000..2cd344248b9690 --- /dev/null +++ b/x-pack/plugins/observability_solution/dataset_quality/public/state_machines/dataset_quality_details_controller/types.ts @@ -0,0 +1,151 @@ +/* + * 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 { DoneInvokeEvent } from 'xstate'; +import type { DegradedFieldSortField } from '../../hooks'; +import { + Dashboard, + DataStreamDetails, + DataStreamSettings, + DegradedField, + DegradedFieldResponse, + NonAggregatableDatasets, +} from '../../../common/api_types'; +import { TableCriteria, TimeRangeConfig } from '../../../common/types'; +import { Integration } from '../../../common/data_streams_stats/integration'; + +export interface DataStream { + name: string; + type: string; + namespace: string; + rawName: string; +} + +export interface DegradedFieldsTableConfig { + table: TableCriteria; + data?: DegradedField[]; +} + +export interface DegradedFieldsWithData { + table: TableCriteria; + data: DegradedField[]; +} + +export interface WithDefaultControllerState { + dataStream: string; + degradedFields: DegradedFieldsTableConfig; + timeRange: TimeRangeConfig; + breakdownField?: string; + isBreakdownFieldEcs?: boolean; + isIndexNotFoundError?: boolean; + integration?: Integration; +} + +export interface WithDataStreamDetails { + dataStreamDetails: DataStreamDetails; +} + +export interface WithBreakdownField { + breakdownField: string | undefined; +} + +export interface WithBreakdownInEcsCheck { + isBreakdownFieldEcs: boolean; +} + +export interface WithDegradedFieldsData { + degradedFields: DegradedFieldsWithData; +} + +export interface WithNonAggregatableDatasetStatus { + isNonAggregatable: boolean; +} + +export interface WithDataStreamSettings { + dataStreamSettings: DataStreamSettings; +} + +export interface WithIntegration { + integration: Integration; + integrationDashboards?: Dashboard[]; +} + +export type DefaultDatasetQualityDetailsContext = Pick< + WithDefaultControllerState, + 'degradedFields' | 'timeRange' | 'isIndexNotFoundError' +>; + +export type DatasetQualityDetailsControllerTypeState = + | { + value: + | 'initializing' + | 'uninitialized' + | 'initializing.nonAggregatableDataset.fetching' + | 'initializing.dataStreamDegradedFields.fetching' + | 'initializing.dataStreamSettings.fetching' + | 'initializing.dataStreamDetails.fetching'; + context: WithDefaultControllerState; + } + | { + value: 'initializing.nonAggregatableDataset.done'; + context: WithDefaultControllerState & WithNonAggregatableDatasetStatus; + } + | { + value: 'initializing.dataStreamDetails.done'; + context: WithDefaultControllerState & WithDataStreamDetails; + } + | { + value: 'initializing.checkBreakdownFieldIsEcs.fetching'; + context: WithDefaultControllerState & WithBreakdownField; + } + | { + value: 'initializing.checkBreakdownFieldIsEcs.done'; + context: WithDefaultControllerState & WithBreakdownInEcsCheck; + } + | { + value: 'initializing.dataStreamDegradedFields.done'; + context: WithDefaultControllerState & WithDegradedFieldsData; + } + | { + value: + | 'initializing.dataStreamSettings.initializeIntegrations' + | 'initializing.dataStreamSettings.initializeIntegrations.integrationDetails.fetching' + | 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.fetching' + | 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.unauthorized'; + context: WithDefaultControllerState & WithDataStreamSettings; + } + | { + value: + | 'initializing.dataStreamSettings.initializeIntegrations.integrationDetails.done' + | 'initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.done'; + context: WithDefaultControllerState & WithDataStreamSettings & WithIntegration; + }; + +export type DatasetQualityDetailsControllerContext = + DatasetQualityDetailsControllerTypeState['context']; + +export type DatasetQualityDetailsControllerEvent = + | { + type: 'UPDATE_TIME_RANGE'; + timeRange: TimeRangeConfig; + } + | { + type: 'BREAKDOWN_FIELD_CHANGE'; + breakdownField: string | undefined; + } + | { + type: 'UPDATE_DEGRADED_FIELDS_TABLE_CRITERIA'; + degraded_field_criteria: TableCriteria; + } + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent; diff --git a/x-pack/plugins/observability_solution/dataset_quality/public/types.ts b/x-pack/plugins/observability_solution/dataset_quality/public/types.ts index bcdd001cb68437..9b97ce12a194f7 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/public/types.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/public/types.ts @@ -15,8 +15,10 @@ import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { ObservabilitySharedPluginSetup } from '@kbn/observability-shared-plugin/public'; import type { FieldsMetadataPublicStart } from '@kbn/fields-metadata-plugin/public'; -import type { CreateDatasetQualityController } from './controller'; import type { DatasetQualityProps } from './components/dataset_quality'; +import { DatasetQualityDetailsProps } from './components/dataset_quality_details'; +import type { CreateDatasetQualityController } from './controller/dataset_quality'; +import type { CreateDatasetQualityDetailsController } from './controller/dataset_quality_details'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DatasetQualityPluginSetup {} @@ -24,6 +26,8 @@ export interface DatasetQualityPluginSetup {} export interface DatasetQualityPluginStart { DatasetQuality: ComponentType; createDatasetQualityController: CreateDatasetQualityController; + DatasetQualityDetails: ComponentType; + createDatasetQualityDetailsController: CreateDatasetQualityDetailsController; } export interface DatasetQualityStartDeps { 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 d89bb83867d10f..e2067dedd26d21 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 @@ -30,15 +30,17 @@ export async function getDataStreamSettings({ }): Promise { throwIfInvalidDataStreamParams(dataStream); - const createdOn = await getDataStreamCreatedOn(esClient, dataStream); - - // Getting the 1st item from the data streams endpoint as we will be passing the exact DS name - const [dataStreamInfo] = await dataStreamService.getMatchingDataStreams(esClient, dataStream); + const [createdOn, [dataStreamInfo], datasetUserPrivileges] = await Promise.all([ + getDataStreamCreatedOn(esClient, dataStream), + dataStreamService.getMatchingDataStreams(esClient, dataStream), + datasetQualityPrivileges.getDatasetPrivileges(esClient, dataStream), + ]); const integration = dataStreamInfo?._meta?.package?.name; return { createdOn, integration, + datasetUserPrivileges, }; } diff --git a/x-pack/plugins/observability_solution/dataset_quality/server/services/data_stream.ts b/x-pack/plugins/observability_solution/dataset_quality/server/services/data_stream.ts index a1b701e524c927..0446e27953af1e 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/server/services/data_stream.ts +++ b/x-pack/plugins/observability_solution/dataset_quality/server/services/data_stream.ts @@ -11,8 +11,6 @@ import type { } from '@elastic/elasticsearch/lib/api/types'; import type { ElasticsearchClient } from '@kbn/core/server'; -import { streamPartsToIndexPattern } from '../../common/utils'; - class DataStreamService { public async getMatchingDataStreams( esClient: ElasticsearchClient, @@ -32,31 +30,6 @@ class DataStreamService { } } - public async getMatchingDataStreamsStats( - esClient: ElasticsearchClient, - dataStreamParts: { - dataset: string; - type: string; - } - ): Promise { - try { - const { data_streams: dataStreamsStats } = await esClient.indices.dataStreamsStats({ - name: streamPartsToIndexPattern({ - typePattern: dataStreamParts.type, - datasetPattern: dataStreamParts.dataset, - }), - human: true, - }); - - return dataStreamsStats; - } catch (e) { - if (e.statusCode === 404) { - return []; - } - throw e; - } - } - public async getStreamsStats( esClient: ElasticsearchClient, dataStreams: string[] diff --git a/x-pack/plugins/observability_solution/dataset_quality/tsconfig.json b/x-pack/plugins/observability_solution/dataset_quality/tsconfig.json index 0397a6f185bf15..04d5362bb98610 100644 --- a/x-pack/plugins/observability_solution/dataset_quality/tsconfig.json +++ b/x-pack/plugins/observability_solution/dataset_quality/tsconfig.json @@ -53,7 +53,9 @@ "@kbn/ebt-tools", "@kbn/fields-metadata-plugin", "@kbn/server-route-repository-utils", - "@kbn/core-analytics-browser" + "@kbn/core-analytics-browser", + "@kbn/core-lifecycle-browser", + "@kbn/core-notifications-browser" ], "exclude": [ "target/**/*" diff --git a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/types.ts b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/types.ts index b4ceb3a174b530..56ee5405841cc0 100644 --- a/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/types.ts +++ b/x-pack/plugins/observability_solution/logs_explorer/public/state_machines/logs_explorer_controller/src/types.ts @@ -156,8 +156,6 @@ export type LogsExplorerControllerTypeState = export type LogsExplorerControllerContext = LogsExplorerControllerTypeState['context']; -export type LogsExplorerControllerStateValue = LogsExplorerControllerTypeState['value']; - export type LogsExplorerControllerEvent = | { type: 'RECEIVED_STATE_CONTAINER'; diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 37642329d35bd9..1cbb69da954637 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -14650,10 +14650,8 @@ "xpack.datasetQuality.expandLabel": "Développer", "xpack.datasetQuality.fetchDatasetDetailsFailed": "Nous n'avons pas pu obtenir les détails de votre ensemble de données.", "xpack.datasetQuality.fetchDatasetDetailsFailed.noDatasetSelected": "Vous n'avez sélectionné aucun ensemble de données", - "xpack.datasetQuality.fetchDatasetSettingsFailed": "Les paramètres de l'ensemble de données n'ont pas pu être chargés.", "xpack.datasetQuality.fetchDatasetStatsFailed": "Nous n'avons pas pu obtenir vos ensembles de données.", "xpack.datasetQuality.fetchDegradedStatsFailed": "Nous n'avons pas pu obtenir d'informations sur vos documents dégradés.", - "xpack.datasetQuality.fetchIntegrationDashboardsFailed": "Nous n'avons pas pu obtenir vos tableaux de bord d'intégration.", "xpack.datasetQuality.fetchIntegrationsFailed": "Nous n'avons pas pu obtenir vos intégrations.", "xpack.datasetQuality.fetchNonAggregatableDatasetsFailed": "Nous n'avons pas pu obtenir d'informations sur les ensembles de données non agrégés.", "xpack.datasetQuality.fewDegradedDocsTooltip": "{degradedDocsCount} documents dégradés dans cet ensemble de données.", @@ -14667,36 +14665,9 @@ "xpack.datasetQuality.flyout.nonAggregatable.warning": "{dataset} est incompatible avec l'agrégation _ignored, ce qui peut entraîner des délais lors de la recherche de données. {howToFixIt}", "xpack.datasetQuality.flyout.nonAggregatableDatasets.link.title": "substitution", "xpack.datasetQuality.flyoutCancelText": "Annuler", - "xpack.datasetQuality.flyoutChartExploreDataInDiscoverText": "Explorer les données dans Discover", - "xpack.datasetQuality.flyoutChartExploreDataInLogsExplorerText": "Explorer les données dans l'Explorateur de logs", - "xpack.datasetQuality.flyoutChartOpenInLensText": "Ouvrir dans Lens", - "xpack.datasetQuality.flyoutDatasetCreatedOnText": "Créé le", - "xpack.datasetQuality.flyoutDatasetDetailsText": "Détails de l’ensemble de données", - "xpack.datasetQuality.flyoutDatasetLastActivityText": "Dernière activité", - "xpack.datasetQuality.flyoutDegradedDocsPercentage": "Pourcentage de documents dégradés", - "xpack.datasetQuality.flyoutDegradedDocsTooltip": "Le pourcentage de documents dégradés (des documents avec la propriété {ignoredProperty}) dans votre ensemble de données.", - "xpack.datasetQuality.flyoutDegradedDocsTopNValues": "{count} principales valeurs de {fieldName}", - "xpack.datasetQuality.flyoutDegradedDocsViz": "Tendance des documents dégradés", - "xpack.datasetQuality.flyoutDegradedFieldsSectionTitle": "Champs dégradés", - "xpack.datasetQuality.flyoutDegradedFieldsSectionTooltip": "Une liste partielle des champs dégradés trouvés dans votre ensemble de données.", - "xpack.datasetQuality.flyoutDegradedFieldsTableLoadingText": "Chargement des champs dégradés", - "xpack.datasetQuality.flyoutDegradedFieldsTableNoData": "Aucun champ dégradé n’a été trouvé", - "xpack.datasetQuality.flyoutDocsCountTotal": "Nombre de documents (total)", - "xpack.datasetQuality.flyoutHostsText": "Hôtes", - "xpack.datasetQuality.flyoutIndexTemplateActionText": "Modèle d'index", - "xpack.datasetQuality.flyoutIntegrationActionsText": "Actions d'intégration", + "xpack.datasetQuality.flyoutDatasetDetailsText": "Détails des ensembles de données", "xpack.datasetQuality.flyoutIntegrationDetailsText": "Détails de l'intégration", "xpack.datasetQuality.flyoutIntegrationNameText": "Nom", - "xpack.datasetQuality.flyoutIntegrationVersionText": "Version", - "xpack.datasetQuality.flyoutOpenInDiscoverText": "Ouvrir dans Discover", - "xpack.datasetQuality.flyoutOpenInLogsExplorerText": "Ouvrir dans l'explorateur de logs", - "xpack.datasetQuality.flyoutSeeIntegrationActionText": "Afficher l'intégration", - "xpack.datasetQuality.flyoutServicesText": "Services", - "xpack.datasetQuality.flyoutShowAllText": "Afficher tout", - "xpack.datasetQuality.flyoutSizeText": "Taille", - "xpack.datasetQuality.flyoutSummaryTitle": "Résumé", - "xpack.datasetQuality.flyoutSummaryTooltip": "Statistiques de l'ensemble de données dans la plage temporelle sélectionnée.", - "xpack.datasetQuality.flyoutViewDashboardsActionText": "Afficher les tableaux de bord", "xpack.datasetQuality.fullDatasetNameDescription": "Activez cette option pour afficher les noms complets des ensembles de données utilisés pour stocker les documents.", "xpack.datasetQuality.fullDatasetNameLabel": "Afficher les noms complets des ensembles de données", "xpack.datasetQuality.inactiveDatasetActivityColumnDescription": "Aucune activité pour la plage temporelle sélectionnée", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dae805d79e18bb..17432ab10a44e3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -14638,10 +14638,8 @@ "xpack.datasetQuality.expandLabel": "拡張", "xpack.datasetQuality.fetchDatasetDetailsFailed": "データセット詳細を取得できませんでした。", "xpack.datasetQuality.fetchDatasetDetailsFailed.noDatasetSelected": "データセットが選択されていません", - "xpack.datasetQuality.fetchDatasetSettingsFailed": "データセット設定を読み込めませんでした。", "xpack.datasetQuality.fetchDatasetStatsFailed": "データセットを取得できませんでした。", "xpack.datasetQuality.fetchDegradedStatsFailed": "劣化したドキュメント情報を取得できませんでした。", - "xpack.datasetQuality.fetchIntegrationDashboardsFailed": "統合ダッシュボードを取得できませんでした。", "xpack.datasetQuality.fetchIntegrationsFailed": "統合を取得できませんでした。", "xpack.datasetQuality.fetchNonAggregatableDatasetsFailed": "集約可能なデータセット情報以外を取得できませんでした。", "xpack.datasetQuality.fewDegradedDocsTooltip": "このデータセットの{degradedDocsCount}個の劣化したドキュメント。", @@ -14655,36 +14653,9 @@ "xpack.datasetQuality.flyout.nonAggregatable.warning": "{dataset}は_ignored集約をサポートしていません。データのクエリを実行するときに遅延が生じる可能性があります。{howToFixIt}", "xpack.datasetQuality.flyout.nonAggregatableDatasets.link.title": "ロールオーバー", "xpack.datasetQuality.flyoutCancelText": "キャンセル", - "xpack.datasetQuality.flyoutChartExploreDataInDiscoverText": "Discoverでデータを探索", - "xpack.datasetQuality.flyoutChartExploreDataInLogsExplorerText": "ログエクスプローラーでデータを探索", - "xpack.datasetQuality.flyoutChartOpenInLensText": "Lensで開く", - "xpack.datasetQuality.flyoutDatasetCreatedOnText": "作成日時", "xpack.datasetQuality.flyoutDatasetDetailsText": "データセット詳細", - "xpack.datasetQuality.flyoutDatasetLastActivityText": "前回のアクティビティ", - "xpack.datasetQuality.flyoutDegradedDocsPercentage": "劣化したドキュメントの割合(%)", - "xpack.datasetQuality.flyoutDegradedDocsTooltip": "データセットにおける劣化したドキュメント({ignoredProperty}プロパティのドキュメント)の割合。", - "xpack.datasetQuality.flyoutDegradedDocsTopNValues": "{fieldName}の上位{count}つの値", - "xpack.datasetQuality.flyoutDegradedDocsViz": "劣化したドキュメントの傾向", - "xpack.datasetQuality.flyoutDegradedFieldsSectionTitle": "劣化したフィールド", - "xpack.datasetQuality.flyoutDegradedFieldsSectionTooltip": "データセットで見つかった劣化したフィールドの部分的なリスト。", - "xpack.datasetQuality.flyoutDegradedFieldsTableLoadingText": "劣化したフィールドを読み込み中", - "xpack.datasetQuality.flyoutDegradedFieldsTableNoData": "劣化したフィールドが見つかりません", - "xpack.datasetQuality.flyoutDocsCountTotal": "ドキュメント数(合計)", - "xpack.datasetQuality.flyoutHostsText": "ホスト", - "xpack.datasetQuality.flyoutIndexTemplateActionText": "インデックステンプレート", - "xpack.datasetQuality.flyoutIntegrationActionsText": "統合アクション", "xpack.datasetQuality.flyoutIntegrationDetailsText": "統合の詳細", "xpack.datasetQuality.flyoutIntegrationNameText": "名前", - "xpack.datasetQuality.flyoutIntegrationVersionText": "Version", - "xpack.datasetQuality.flyoutOpenInDiscoverText": "Discoverで開く", - "xpack.datasetQuality.flyoutOpenInLogsExplorerText": "ログエクスプローラーで開く", - "xpack.datasetQuality.flyoutSeeIntegrationActionText": "統合を表示", - "xpack.datasetQuality.flyoutServicesText": "サービス", - "xpack.datasetQuality.flyoutShowAllText": "すべて表示", - "xpack.datasetQuality.flyoutSizeText": "サイズ", - "xpack.datasetQuality.flyoutSummaryTitle": "まとめ", - "xpack.datasetQuality.flyoutSummaryTooltip": "選択した時間範囲内のデータセットの統計情報", - "xpack.datasetQuality.flyoutViewDashboardsActionText": "ダッシュボードを表示", "xpack.datasetQuality.fullDatasetNameDescription": "オンにすると、ドキュメントを格納するために使用される実際のデータセット名が表示されます。", "xpack.datasetQuality.fullDatasetNameLabel": "詳細なデータセット名を表示", "xpack.datasetQuality.inactiveDatasetActivityColumnDescription": "選択したタイムフレームにアクティビティがありません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9df2e11f620cc5..aafc9f26c90440 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -14661,10 +14661,8 @@ "xpack.datasetQuality.expandLabel": "展开", "xpack.datasetQuality.fetchDatasetDetailsFailed": "无法获取数据集详情。", "xpack.datasetQuality.fetchDatasetDetailsFailed.noDatasetSelected": "尚未选择任何数据集", - "xpack.datasetQuality.fetchDatasetSettingsFailed": "无法加载数据集设置。", "xpack.datasetQuality.fetchDatasetStatsFailed": "无法获取数据集。", "xpack.datasetQuality.fetchDegradedStatsFailed": "无法获取已降级文档信息。", - "xpack.datasetQuality.fetchIntegrationDashboardsFailed": "无法获取集成仪表板。", "xpack.datasetQuality.fetchIntegrationsFailed": "无法获取集成。", "xpack.datasetQuality.fetchNonAggregatableDatasetsFailed": "无法获取非可聚合数据集信息。", "xpack.datasetQuality.fewDegradedDocsTooltip": "此数据集中的 {degradedDocsCount} 个已降级文档。", @@ -14678,36 +14676,9 @@ "xpack.datasetQuality.flyout.nonAggregatable.warning": "{dataset} 不支持 _ignored 聚合,在查询数据时可能会导致延迟。{howToFixIt}", "xpack.datasetQuality.flyout.nonAggregatableDatasets.link.title": "滚动更新", "xpack.datasetQuality.flyoutCancelText": "取消", - "xpack.datasetQuality.flyoutChartExploreDataInDiscoverText": "在 Discover 中浏览数据", - "xpack.datasetQuality.flyoutChartExploreDataInLogsExplorerText": "在日志浏览器中浏览数据", - "xpack.datasetQuality.flyoutChartOpenInLensText": "在 Lens 中打开", - "xpack.datasetQuality.flyoutDatasetCreatedOnText": "创建日期", "xpack.datasetQuality.flyoutDatasetDetailsText": "数据集详情", - "xpack.datasetQuality.flyoutDatasetLastActivityText": "上次活动", - "xpack.datasetQuality.flyoutDegradedDocsPercentage": "已降级文档 %", - "xpack.datasetQuality.flyoutDegradedDocsTooltip": "您的数据集中已降级文档的百分比,即包含 {ignoredProperty} 属性的文档。", - "xpack.datasetQuality.flyoutDegradedDocsTopNValues": "{fieldName} 的排名前 {count} 值", - "xpack.datasetQuality.flyoutDegradedDocsViz": "已降级文档趋势", - "xpack.datasetQuality.flyoutDegradedFieldsSectionTitle": "已降级字段", - "xpack.datasetQuality.flyoutDegradedFieldsSectionTooltip": "在数据集中发现的已降级字段的部分列表。", - "xpack.datasetQuality.flyoutDegradedFieldsTableLoadingText": "正在加载已降级字段", - "xpack.datasetQuality.flyoutDegradedFieldsTableNoData": "找不到已降级字段", - "xpack.datasetQuality.flyoutDocsCountTotal": "文档计数(总计)", - "xpack.datasetQuality.flyoutHostsText": "主机", - "xpack.datasetQuality.flyoutIndexTemplateActionText": "索引模板", - "xpack.datasetQuality.flyoutIntegrationActionsText": "集成操作", "xpack.datasetQuality.flyoutIntegrationDetailsText": "集成详情", "xpack.datasetQuality.flyoutIntegrationNameText": "名称", - "xpack.datasetQuality.flyoutIntegrationVersionText": "版本", - "xpack.datasetQuality.flyoutOpenInDiscoverText": "在 Discover 中打开", - "xpack.datasetQuality.flyoutOpenInLogsExplorerText": "在日志浏览器中打开", - "xpack.datasetQuality.flyoutSeeIntegrationActionText": "查看集成", - "xpack.datasetQuality.flyoutServicesText": "服务", - "xpack.datasetQuality.flyoutShowAllText": "全部显示", - "xpack.datasetQuality.flyoutSizeText": "大小", - "xpack.datasetQuality.flyoutSummaryTitle": "摘要", - "xpack.datasetQuality.flyoutSummaryTooltip": "选定时间范围内的数据集统计信息。", - "xpack.datasetQuality.flyoutViewDashboardsActionText": "查看仪表板", "xpack.datasetQuality.fullDatasetNameDescription": "打开以显示用于存储文档的实际数据集名称。", "xpack.datasetQuality.fullDatasetNameLabel": "显示完整数据集名称", "xpack.datasetQuality.inactiveDatasetActivityColumnDescription": "选定时间范围内没有任何活动", 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 e5b38d2463cb0a..3ba9965d980a47 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 @@ -35,6 +35,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { version: '1.14.0', }; + const defaultDataStreamPrivileges = { + datasetUserPrivileges: { canRead: true, canMonitor: true, canViewIntegrations: true }, + }; + async function callApiAs(user: DatasetQualityApiClientKey, dataStream: string) { return await datasetQualityApiClient[user]({ endpoint: 'GET /internal/dataset_quality/data_streams/{dataStream}/settings', @@ -99,11 +103,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(err.res.body.message.indexOf(expectedMessage)).to.greaterThan(-1); }); - it('returns {} if matching data stream is not available', async () => { + it('returns only privileges if matching data stream is not available', async () => { const nonExistentDataSet = 'Non-existent'; const nonExistentDataStream = `${type}-${nonExistentDataSet}-${namespace}`; const resp = await callApiAs('datasetQualityLogsUser', nonExistentDataStream); - expect(resp.body).empty(); + expect(resp.body).eql(defaultDataStreamPrivileges); }); it('returns "createdOn" correctly', async () => { @@ -136,6 +140,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); 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 + ); }); after(async () => { 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 c7550395b303c8..37bfc688c9ec94 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 @@ -29,6 +29,10 @@ export default function ({ getService }: DatasetQualityFtrContextProvider) { const serviceName = 'my-service'; const hostName = 'synth-host'; + const defaultDataStreamPrivileges = { + datasetUserPrivileges: { canRead: true, canMonitor: true, canViewIntegrations: true }, + }; + async function callApi( dataStream: string, roleAuthc: RoleCredentials, @@ -85,11 +89,11 @@ export default function ({ getService }: DatasetQualityFtrContextProvider) { expect(err.res.body.message.indexOf(expectedMessage)).to.greaterThan(-1); }); - it('returns {} if matching data stream is not available', async () => { + it('returns only privileges if matching data stream is not available', async () => { const nonExistentDataSet = 'Non-existent'; const nonExistentDataStream = `${type}-${nonExistentDataSet}-${namespace}`; const resp = await callApi(nonExistentDataStream, roleAuthc, internalReqHeader); - expect(resp.body).empty(); + expect(resp.body).eql(defaultDataStreamPrivileges); }); it('returns "createdOn" correctly', async () => {