From 288e365a58eb0d3bcfbcfa14ef58668c7afe06e8 Mon Sep 17 00:00:00 2001 From: Yngrid Coello Date: Wed, 31 Jan 2024 13:45:53 +0100 Subject: [PATCH] [Dataset quality] State management (#174906) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## :memo: Summary This PR contains `dataset-quality` plugin state management, we have decided to go with xstate. The general idea, and following https://github.com/elastic/kibana/pull/170200 as inspiration, we wanted to detach `dataset-quality` plugin state from its consumers, in this way our plugin wouldn't perform side effect, such as updating routes, outside its scope. The flow of the information now looks like image Current internal state of the plugin is fairly simple but it's envisioned to grow in a near future with for example filters, flyout options, etc. ## 💡 Notes for Logs UX reviewers ### Dataset Quality plugin #### Goals **Decoupling from global state**: The primary goal is to decouple the `` component from direct dependencies on the URL and other page-wide side effects. Decoupling from global state enables its use in other applications without interfering with their page state. **Stable and Strictly Typed Public API**: Introduce a public API for the `` component. This API provides consumers the ability to subscribe to the component's state and/or initialize the state from the outside. #### Architecture The architecture of the `` plugin has been designed to provide a modular structure, with a focus on robust state management. This structure is primarily composed of the uncontrolled `` component and a separately instantiable controller. **Uncontrolled `` component**: The `` is designed as an uncontrolled component. All the logic around this component is handled by hooks, such as `useDatasetQualityTable` with the ability to change or replace the underlying dependencies. **Separately instantiable controller**: A controller is introduced to encapsulate and manage the business logic associated with the `` component. The controller centralizes the business logic, separating it from the UI layer. This provides flexibility in managing different instances of the `` and reusability from different consumers. #### App statechart image ### Observability Logs Explorer App #### Goals **URL persistence**: Implement a versioned data structure for URL persistence, this give us the flexibility to extend or change the app state without workarounds. The general idea of having the public state of the dataset quality plugin stored in the URL of this consumer is the ability to share an exact state with colleagues, customers, etc. #### Changes **Introduction of versioned URL schema**: This new schema will standarize how URL-based state is managed, providing a clear and consistent mechanism for encoding and decoding state information in the URL. The versioning will allow us to evolve the data structure in a backwards-compatible way incorporate features added or changed in the future. **Page-level statechart implementation**: This introduces a page-level statechart to orchestrate the initialization and instantiation of the `` controller. #### App statechart image --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../dataset_quality/common/api_types.ts | 3 +- .../dataset_quality/common/constants.ts | 2 + .../common/data_streams_stats/integration.ts | 4 +- .../common/data_streams_stats/types.ts | 2 +- .../components/dataset_quality/columns.tsx | 13 +- .../components/dataset_quality/context.ts | 4 +- .../dataset_quality/dataset_quality.tsx | 29 +- .../components/dataset_quality/header.tsx | 4 +- .../components/dataset_quality/table.tsx | 8 +- .../components/flyout/dataset_summary.tsx | 13 +- .../public/components/flyout/flyout.tsx | 33 +-- .../public/components/flyout/header.tsx | 4 +- .../public/components/flyout/types.ts | 13 + .../public/controller/create_controller.ts | 66 +++++ .../public/controller/index.ts | 10 + .../controller/lazy_create_controller.ts | 15 ++ .../public/controller/provider.ts | 15 ++ .../public/controller/public_state.ts | 42 +++ .../public/controller/types.ts | 41 +++ .../hooks/use_dataset_quality_flyout.tsx | 57 +--- .../hooks/use_dataset_quality_table.tsx | 146 +++++----- .../public/hooks/use_link_to_log_explorer.ts | 7 +- .../plugins/dataset_quality/public/plugin.tsx | 8 +- .../data_streams_stats_client.ts | 10 +- .../services/data_streams_stats/types.ts | 2 +- .../dataset_quality_controller/index.ts} | 4 +- .../src/defaults.ts | 22 ++ .../dataset_quality_controller/src/index.ts | 10 + .../src/notifications.ts | 43 +++ .../src/state_machine.ts | 254 ++++++++++++++++++ .../dataset_quality_controller/src/types.ts | 122 +++++++++ .../plugins/dataset_quality/public/types.ts | 5 +- .../merge_degraded_docs_into_datastreams.ts | 26 ++ .../get_data_stream_details/index.ts | 6 +- .../server/routes/data_streams/routes.ts | 28 +- .../server/services/data_stream.ts | 15 +- x-pack/plugins/dataset_quality/tsconfig.json | 3 +- .../common/index.ts | 7 +- .../locators/utils/construct_locator_path.ts | 9 +- .../common/url_schema/common.ts | 1 + .../dataset_quality/url_schema_v1.ts | 63 +++++ .../common/url_schema/index.ts | 8 +- .../observability_log_explorer.tsx | 6 +- .../routes/main/dataset_quality_route.tsx | 98 ++++++- .../dataset_quality/src/controller_service.ts | 70 +++++ .../dataset_quality/src/defaults.ts | 12 + .../dataset_quality/src/provider.ts | 30 +++ .../dataset_quality/src/state_machine.ts | 146 ++++++++++ .../dataset_quality/src/types.ts | 54 ++++ .../dataset_quality/src/url_schema_v1.ts | 41 +++ .../src/url_state_storage_service.ts | 75 ++++++ .../src/url_schema_v1.ts | 17 +- .../data_streams/data_stream_details.spec.ts | 20 +- .../observability_log_explorer.ts | 8 +- 54 files changed, 1505 insertions(+), 249 deletions(-) create mode 100644 x-pack/plugins/dataset_quality/public/components/flyout/types.ts create mode 100644 x-pack/plugins/dataset_quality/public/controller/create_controller.ts create mode 100644 x-pack/plugins/dataset_quality/public/controller/index.ts create mode 100644 x-pack/plugins/dataset_quality/public/controller/lazy_create_controller.ts create mode 100644 x-pack/plugins/dataset_quality/public/controller/provider.ts create mode 100644 x-pack/plugins/dataset_quality/public/controller/public_state.ts create mode 100644 x-pack/plugins/dataset_quality/public/controller/types.ts rename x-pack/plugins/dataset_quality/{common/data_streams_stats/data_stream_details.ts => public/state_machines/dataset_quality_controller/index.ts} (80%) create mode 100644 x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/defaults.ts create mode 100644 x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/index.ts create mode 100644 x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts create mode 100644 x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts create mode 100644 x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts create mode 100644 x-pack/plugins/dataset_quality/public/utils/merge_degraded_docs_into_datastreams.ts create mode 100644 x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/dataset_quality/url_schema_v1.ts create mode 100644 x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/controller_service.ts create mode 100644 x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/defaults.ts create mode 100644 x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/provider.ts create mode 100644 x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/state_machine.ts create mode 100644 x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/types.ts create mode 100644 x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/url_schema_v1.ts create mode 100644 x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/url_state_storage_service.ts diff --git a/x-pack/plugins/dataset_quality/common/api_types.ts b/x-pack/plugins/dataset_quality/common/api_types.ts index 7be4481b40878..0c88b26839eae 100644 --- a/x-pack/plugins/dataset_quality/common/api_types.ts +++ b/x-pack/plugins/dataset_quality/common/api_types.ts @@ -54,8 +54,9 @@ export const degradedDocsRt = rt.type({ export type DegradedDocs = rt.TypeOf; -export const dataStreamDetailsRt = rt.type({ +export const dataStreamDetailsRt = rt.partial({ createdOn: rt.number, + lastActivity: rt.number, }); export type DataStreamDetails = rt.TypeOf; diff --git a/x-pack/plugins/dataset_quality/common/constants.ts b/x-pack/plugins/dataset_quality/common/constants.ts index 4808ac4325753..88ca32189994e 100644 --- a/x-pack/plugins/dataset_quality/common/constants.ts +++ b/x-pack/plugins/dataset_quality/common/constants.ts @@ -10,3 +10,5 @@ export const DEFAULT_DATASET_TYPE = 'logs'; export const POOR_QUALITY_MINIMUM_PERCENTAGE = 3; export const DEGRADED_QUALITY_MINIMUM_PERCENTAGE = 0; +export const DEFAULT_SORT_FIELD = 'title'; +export const DEFAULT_SORT_DIRECTION = 'asc'; diff --git a/x-pack/plugins/dataset_quality/common/data_streams_stats/integration.ts b/x-pack/plugins/dataset_quality/common/data_streams_stats/integration.ts index 937efd407e6fc..68a394c4b41c5 100644 --- a/x-pack/plugins/dataset_quality/common/data_streams_stats/integration.ts +++ b/x-pack/plugins/dataset_quality/common/data_streams_stats/integration.ts @@ -9,8 +9,8 @@ import { IntegrationType } from './types'; export class Integration { name: IntegrationType['name']; - title: IntegrationType['title']; - version: IntegrationType['version']; + title: string; + version: string; icons?: IntegrationType['icons']; private constructor(integration: Integration) { diff --git a/x-pack/plugins/dataset_quality/common/data_streams_stats/types.ts b/x-pack/plugins/dataset_quality/common/data_streams_stats/types.ts index c7549d592a000..d213e5e021292 100644 --- a/x-pack/plugins/dataset_quality/common/data_streams_stats/types.ts +++ b/x-pack/plugins/dataset_quality/common/data_streams_stats/types.ts @@ -33,4 +33,4 @@ export type GetDataStreamDetailsResponse = APIReturnType<`GET /internal/dataset_quality/data_streams/{dataStream}/details`>; export type { DataStreamStat } from './data_stream_stat'; -export type { DataStreamDetails } from './data_stream_details'; +export type { DataStreamDetails } from '../api_types'; diff --git a/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx b/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx index b4e9035fef215..2f2febddc1150 100644 --- a/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx +++ b/x-pack/plugins/dataset_quality/public/components/dataset_quality/columns.tsx @@ -22,7 +22,7 @@ import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React, { Dispatch, SetStateAction } from 'react'; +import React from 'react'; import { css } from '@emotion/react'; import { DEGRADED_QUALITY_MINIMUM_PERCENTAGE, @@ -32,6 +32,7 @@ import { DataStreamStat } from '../../../common/data_streams_stats/data_stream_s import { QualityIndicator, QualityPercentageIndicator } from '../quality_indicator'; import { IntegrationIcon } from '../common'; import { useLinkToLogExplorer } from '../../hooks'; +import { FlyoutDataset } from '../../state_machines/dataset_quality_controller'; const expandDatasetAriaLabel = i18n.translate('xpack.datasetQuality.expandLabel', { defaultMessage: 'Expand', @@ -108,25 +109,25 @@ const degradedDocsColumnTooltip = ( export const getDatasetQualityTableColumns = ({ fieldFormats, selectedDataset, - setSelectedDataset, + openFlyout, loadingDegradedStats, }: { fieldFormats: FieldFormatsStart; - selectedDataset?: DataStreamStat; + selectedDataset?: FlyoutDataset; loadingDegradedStats?: boolean; - setSelectedDataset: Dispatch>; + openFlyout: (selectedDataset: FlyoutDataset) => void; }): Array> => { return [ { name: '', render: (dataStreamStat: DataStreamStat) => { - const isExpanded = dataStreamStat === selectedDataset; + const isExpanded = dataStreamStat.rawName === selectedDataset?.rawName; return ( setSelectedDataset(isExpanded ? undefined : dataStreamStat)} + onClick={() => openFlyout(dataStreamStat as FlyoutDataset)} iconType={isExpanded ? 'minimize' : 'expand'} title={!isExpanded ? expandDatasetAriaLabel : collapseDatasetAriaLabel} aria-label={!isExpanded ? expandDatasetAriaLabel : collapseDatasetAriaLabel} diff --git a/x-pack/plugins/dataset_quality/public/components/dataset_quality/context.ts b/x-pack/plugins/dataset_quality/public/components/dataset_quality/context.ts index 64029b649a58d..9d89c8522d7dc 100644 --- a/x-pack/plugins/dataset_quality/public/components/dataset_quality/context.ts +++ b/x-pack/plugins/dataset_quality/public/components/dataset_quality/context.ts @@ -5,10 +5,10 @@ * 2.0. */ import { createContext, useContext } from 'react'; -import { IDataStreamsStatsClient } from '../../services/data_streams_stats'; +import { DatasetQualityControllerStateService } from '../../state_machines/dataset_quality_controller'; export interface DatasetQualityContextValue { - dataStreamsStatsServiceClient: IDataStreamsStatsClient; + service: DatasetQualityControllerStateService; } export const DatasetQualityContext = createContext({} as DatasetQualityContextValue); diff --git a/x-pack/plugins/dataset_quality/public/components/dataset_quality/dataset_quality.tsx b/x-pack/plugins/dataset_quality/public/components/dataset_quality/dataset_quality.tsx index 9fe6ca8db3b2f..40b07a1e1aa7e 100644 --- a/x-pack/plugins/dataset_quality/public/components/dataset_quality/dataset_quality.tsx +++ b/x-pack/plugins/dataset_quality/public/components/dataset_quality/dataset_quality.tsx @@ -4,15 +4,18 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { CoreStart } from '@kbn/core/public'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { DataStreamsStatsService } from '../../services/data_streams_stats/data_streams_stats_service'; +import { dynamic } from '@kbn/shared-ux-utility'; import { DatasetQualityContext, DatasetQualityContextValue } from './context'; import { useKibanaContextForPluginProvider } from '../../utils'; import { DatasetQualityStartDeps } from '../../types'; -import { Header } from './header'; -import { Table } from './table'; +import { DatasetQualityController } from '../../controller'; + +export interface DatasetQualityProps { + controller: DatasetQualityController; +} export interface CreateDatasetQualityArgs { core: CoreStart; @@ -20,16 +23,15 @@ export interface CreateDatasetQualityArgs { } export const createDatasetQuality = ({ core, plugins }: CreateDatasetQualityArgs) => { - return () => { + return ({ controller }: DatasetQualityProps) => { const KibanaContextProviderForPlugin = useKibanaContextForPluginProvider(core, plugins); - const dataStreamsStatsServiceClient = new DataStreamsStatsService().start({ - http: core.http, - }).client; - - const datasetQualityProviderValue: DatasetQualityContextValue = { - dataStreamsStatsServiceClient, - }; + const datasetQualityProviderValue: DatasetQualityContextValue = useMemo( + () => ({ + service: controller.service, + }), + [controller.service] + ); return ( @@ -41,6 +43,9 @@ export const createDatasetQuality = ({ core, plugins }: CreateDatasetQualityArgs }; }; +const Header = dynamic(() => import('./header')); +const Table = dynamic(() => import('./table')); + function DatasetQuality() { return ( diff --git a/x-pack/plugins/dataset_quality/public/components/dataset_quality/header.tsx b/x-pack/plugins/dataset_quality/public/components/dataset_quality/header.tsx index 23753941cbe49..51684cd641074 100644 --- a/x-pack/plugins/dataset_quality/public/components/dataset_quality/header.tsx +++ b/x-pack/plugins/dataset_quality/public/components/dataset_quality/header.tsx @@ -9,6 +9,8 @@ import { EuiPageHeader } from '@elastic/eui'; import React from 'react'; import { datasetQualityAppTitle } from '../../../common/translations'; -export function Header() { +// Allow for lazy loading +// eslint-disable-next-line import/no-default-export +export default function Header() { return ; } diff --git a/x-pack/plugins/dataset_quality/public/components/dataset_quality/table.tsx b/x-pack/plugins/dataset_quality/public/components/dataset_quality/table.tsx index c78fa66e3f6fc..4779453a14d6c 100644 --- a/x-pack/plugins/dataset_quality/public/components/dataset_quality/table.tsx +++ b/x-pack/plugins/dataset_quality/public/components/dataset_quality/table.tsx @@ -8,9 +8,11 @@ import React from 'react'; import { EuiBasicTable, EuiHorizontalRule, EuiSpacer, EuiText, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; +import { dynamic } from '@kbn/shared-ux-utility'; import { loadingDatasetsText, noDatasetsTitle } from '../../../common/translations'; import { useDatasetQualityTable } from '../../hooks'; -import { Flyout } from '../flyout'; + +const Flyout = dynamic(() => import('../flyout/flyout')); export const Table = () => { const { @@ -69,3 +71,7 @@ export const Table = () => { ); }; + +// Allow for lazy loading +// eslint-disable-next-line import/no-default-export +export default Table; diff --git a/x-pack/plugins/dataset_quality/public/components/flyout/dataset_summary.tsx b/x-pack/plugins/dataset_quality/public/components/flyout/dataset_summary.tsx index 17691f57dbddf..a0954ab44e49b 100644 --- a/x-pack/plugins/dataset_quality/public/components/flyout/dataset_summary.tsx +++ b/x-pack/plugins/dataset_quality/public/components/flyout/dataset_summary.tsx @@ -8,29 +8,26 @@ import React from 'react'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types'; +import { DataStreamDetails } from '../../../common/data_streams_stats'; import { flyoutDatasetCreatedOnText, flyoutDatasetDetailsText, flyoutDatasetLastActivityText, } from '../../../common/translations'; -import { DataStreamStat, DataStreamDetails } from '../../../common/data_streams_stats'; import { FieldsList, FieldsListLoading } from './fields_list'; interface DatasetSummaryProps { fieldFormats: FieldFormatsStart; dataStreamDetails?: DataStreamDetails; - dataStreamStat: DataStreamStat; } -export function DatasetSummary({ - dataStreamStat, - dataStreamDetails, - fieldFormats, -}: DatasetSummaryProps) { +export function DatasetSummary({ dataStreamDetails, fieldFormats }: DatasetSummaryProps) { const dataFormatter = fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.DATE, [ ES_FIELD_TYPES.DATE, ]); - const formattedLastActivity = dataFormatter.convert(dataStreamStat.lastActivity); + const formattedLastActivity = dataStreamDetails?.lastActivity + ? dataFormatter.convert(dataStreamDetails?.lastActivity) + : '-'; const formattedCreatedOn = dataStreamDetails?.createdOn ? dataFormatter.convert(dataStreamDetails.createdOn) : '-'; diff --git a/x-pack/plugins/dataset_quality/public/components/flyout/flyout.tsx b/x-pack/plugins/dataset_quality/public/components/flyout/flyout.tsx index eaccf05a02ae4..9e2e6b2192c80 100644 --- a/x-pack/plugins/dataset_quality/public/components/flyout/flyout.tsx +++ b/x-pack/plugins/dataset_quality/public/components/flyout/flyout.tsx @@ -15,46 +15,29 @@ import { EuiSpacer, } from '@elastic/eui'; import React, { Fragment } from 'react'; -import { DEFAULT_DATASET_TYPE } from '../../../common/constants'; -import { DataStreamStat } from '../../../common/data_streams_stats/data_stream_stat'; import { flyoutCancelText } from '../../../common/translations'; import { useDatasetQualityFlyout } from '../../hooks'; import { DatasetSummary, DatasetSummaryLoading } from './dataset_summary'; import { Header } from './header'; import { IntegrationSummary } from './integration_summary'; +import { FlyoutProps } from './types'; -interface FlyoutProps { - dataset: DataStreamStat; - closeFlyout: () => void; -} - -export function Flyout({ dataset, closeFlyout }: FlyoutProps) { - const { - dataStreamStat, - dataStreamDetails, - dataStreamStatLoading, - dataStreamDetailsLoading, - fieldFormats, - } = useDatasetQualityFlyout({ - type: DEFAULT_DATASET_TYPE, - dataset: dataset.name, - namespace: dataset.namespace, - }); +// Allow for lazy loading +// eslint-disable-next-line import/no-default-export +export default function Flyout({ dataset, closeFlyout }: FlyoutProps) { + const { dataStreamStat, dataStreamDetails, dataStreamDetailsLoading, fieldFormats } = + useDatasetQualityFlyout(); return ( <>
- {dataStreamStatLoading || dataStreamDetailsLoading ? ( + {dataStreamDetailsLoading ? ( ) : dataStreamStat ? ( - + {dataStreamStat.integration && ( diff --git a/x-pack/plugins/dataset_quality/public/components/flyout/header.tsx b/x-pack/plugins/dataset_quality/public/components/flyout/header.tsx index 7e87b2a5a62c9..2559c62d26991 100644 --- a/x-pack/plugins/dataset_quality/public/components/flyout/header.tsx +++ b/x-pack/plugins/dataset_quality/public/components/flyout/header.tsx @@ -17,11 +17,11 @@ import { import { css } from '@emotion/react'; import React from 'react'; import { flyoutOpenInLogExplorerText } from '../../../common/translations'; -import { DataStreamStat } from '../../../common/data_streams_stats/data_stream_stat'; import { useLinkToLogExplorer } from '../../hooks'; +import { FlyoutDataset } from '../../state_machines/dataset_quality_controller'; import { IntegrationIcon } from '../common'; -export function Header({ dataStreamStat }: { dataStreamStat: DataStreamStat }) { +export function Header({ dataStreamStat }: { dataStreamStat: FlyoutDataset }) { const { integration, title } = dataStreamStat; const euiShadow = useEuiShadow('s'); const { euiTheme } = useEuiTheme(); diff --git a/x-pack/plugins/dataset_quality/public/components/flyout/types.ts b/x-pack/plugins/dataset_quality/public/components/flyout/types.ts new file mode 100644 index 0000000000000..97d82b862428d --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/components/flyout/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FlyoutDataset } from '../../state_machines/dataset_quality_controller'; + +export interface FlyoutProps { + dataset: FlyoutDataset; + closeFlyout: () => void; +} diff --git a/x-pack/plugins/dataset_quality/public/controller/create_controller.ts b/x-pack/plugins/dataset_quality/public/controller/create_controller.ts new file mode 100644 index 0000000000000..280d4092ba327 --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/controller/create_controller.ts @@ -0,0 +1,66 @@ +/* + * 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 { DataStreamsStatsService } from '../services/data_streams_stats'; +import { + createDatasetQualityControllerStateMachine, + DEFAULT_CONTEXT, +} from '../state_machines/dataset_quality_controller'; +import { DatasetQualityStartDeps } from '../types'; +import { getContextFromPublicState, getPublicStateFromContext } from './public_state'; +import { DatasetQualityController, DatasetQualityPublicStateUpdate } from './types'; + +type InitialState = DatasetQualityPublicStateUpdate; + +interface Dependencies { + core: CoreStart; + plugins: DatasetQualityStartDeps; +} + +export const createDatasetQualityControllerFactory = + ({ core }: Dependencies) => + async ({ + initialState = DEFAULT_CONTEXT, + }: { + initialState?: InitialState; + }): Promise => { + const initialContext = getContextFromPublicState(initialState ?? {}); + + const dataStreamStatsClient = new DataStreamsStatsService().start({ + http: core.http, + }).client; + + const machine = createDatasetQualityControllerStateMachine({ + initialContext, + toasts: core.notifications.toasts, + dataStreamStatsClient, + }); + + const service = interpret(machine, { + devTools: getDevToolsOptions(), + }); + + const state$ = from(service).pipe( + map(({ context }) => getPublicStateFromContext(context)), + distinctUntilChanged(equal) + ); + + return { + state$, + service, + }; + }; + +export type CreateDatasetQualityControllerFactory = typeof createDatasetQualityControllerFactory; +export type CreateDatasetQualityController = ReturnType< + typeof createDatasetQualityControllerFactory +>; diff --git a/x-pack/plugins/dataset_quality/public/controller/index.ts b/x-pack/plugins/dataset_quality/public/controller/index.ts new file mode 100644 index 0000000000000..88f30388847e7 --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/controller/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './create_controller'; +export * from './provider'; +export * from './types'; diff --git a/x-pack/plugins/dataset_quality/public/controller/lazy_create_controller.ts b/x-pack/plugins/dataset_quality/public/controller/lazy_create_controller.ts new file mode 100644 index 0000000000000..2074b375022e2 --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/controller/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 type { CreateDatasetQualityControllerFactory } from './create_controller'; + +export const createDatasetQualityControllerLazyFactory: CreateDatasetQualityControllerFactory = + (dependencies) => async (args) => { + const { createDatasetQualityControllerFactory } = await import('./create_controller'); + + return createDatasetQualityControllerFactory(dependencies)(args); + }; diff --git a/x-pack/plugins/dataset_quality/public/controller/provider.ts b/x-pack/plugins/dataset_quality/public/controller/provider.ts new file mode 100644 index 0000000000000..f5aee7551e69f --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/controller/provider.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 createContainer from 'constate'; +import type { DatasetQualityController } from './types'; + +const useDatasetQualityController = ({ controller }: { controller: DatasetQualityController }) => + controller; + +export const [DatasetQualityControllerProvider, useDatasetQualityControllerContext] = + createContainer(useDatasetQualityController); diff --git a/x-pack/plugins/dataset_quality/public/controller/public_state.ts b/x-pack/plugins/dataset_quality/public/controller/public_state.ts new file mode 100644 index 0000000000000..b9f57d75441ac --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/controller/public_state.ts @@ -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 { SortField } from '../hooks'; +import { + DatasetQualityControllerContext, + DEFAULT_CONTEXT, +} from '../state_machines/dataset_quality_controller'; +import { DatasetQualityPublicState, DatasetQualityPublicStateUpdate } from './types'; + +export const getPublicStateFromContext = ( + context: DatasetQualityControllerContext +): DatasetQualityPublicState => { + return { + table: context.table, + flyout: context.flyout, + }; +}; + +export const getContextFromPublicState = ( + publicState: DatasetQualityPublicStateUpdate +): DatasetQualityControllerContext => ({ + ...DEFAULT_CONTEXT, + table: { + ...DEFAULT_CONTEXT.table, + ...publicState.table, + sort: publicState.table?.sort + ? { + ...publicState.table?.sort, + field: publicState.table?.sort.field as SortField, + } + : DEFAULT_CONTEXT.table.sort, + }, + flyout: { + ...DEFAULT_CONTEXT.flyout, + ...publicState.flyout, + }, +}); diff --git a/x-pack/plugins/dataset_quality/public/controller/types.ts b/x-pack/plugins/dataset_quality/public/controller/types.ts new file mode 100644 index 0000000000000..3029812e9571e --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/controller/types.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Observable } from 'rxjs'; +import { DataStreamStat } from '../../common/data_streams_stats'; +import { Direction } from '../hooks'; +import { DatasetQualityControllerStateService } from '../state_machines/dataset_quality_controller'; + +export interface DatasetQualityController { + state$: Observable; + service: DatasetQualityControllerStateService; +} + +export interface DatasetQualityTableOptions { + page?: number; + rowsPerPage?: number; + sort?: { + field: string; + direction: Direction; + }; +} + +type FlyoutOptions = Omit< + DataStreamStat, + 'type' | 'size' | 'sizeBytes' | 'lastActivity' | 'degradedDocs' +>; + +export interface DatasetQualityFlyoutOptions { + dataset?: FlyoutOptions & { type: string }; +} + +export interface DatasetQualityPublicState { + table: DatasetQualityTableOptions; + flyout: DatasetQualityFlyoutOptions; +} + +export type DatasetQualityPublicStateUpdate = Partial; diff --git a/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_flyout.tsx b/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_flyout.tsx index 6f1cff66c6df7..a09fd06adf220 100644 --- a/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_flyout.tsx +++ b/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_flyout.tsx @@ -5,60 +5,29 @@ * 2.0. */ -import { useMemo } from 'react'; -import { useFetcher } from '@kbn/observability-shared-plugin/public'; -import { DataStreamStatServiceResponse } from '../../common/data_streams_stats'; -import { DataStreamNameParts, dataStreamPartsToIndexName } from '../../common/utils'; +import { useSelector } from '@xstate/react'; import { useDatasetQualityContext } from '../components/dataset_quality/context'; import { useKibanaContextForPlugin } from '../utils'; -export const useDatasetQualityFlyout = ({ type, dataset, namespace }: DataStreamNameParts) => { +export const useDatasetQualityFlyout = () => { const { services: { fieldFormats }, } = useKibanaContextForPlugin(); - const { dataStreamsStatsServiceClient: client } = useDatasetQualityContext(); - const { data: dataStreamStatList = [], loading: dataStreamStatLoading } = useFetcher( - async () => client.getDataStreamsStats({ datasetQuery: `${dataset}-${namespace}`, type }), - [dataset, namespace, type] - ); - const { data: dataStreamDetails = {}, loading: dataStreamDetailsLoading } = useFetcher( - async () => - client.getDataStreamDetails({ - dataStream: dataStreamPartsToIndexName({ type, dataset, namespace }), - }), - [dataset, namespace, type] - ); + const { service } = useDatasetQualityContext(); - return useMemo(() => { - const isDataStreamStatStale = isStaleData({ type, dataset, namespace }, dataStreamStatList[0]); + const { dataset: dataStreamStat, datasetDetails: dataStreamDetails } = useSelector( + service, + (state) => state.context.flyout + ); + const dataStreamDetailsLoading = useSelector(service, (state) => + state.matches('datasets.loaded.flyoutOpen.fetching') + ); - return { - dataStreamStat: isDataStreamStatStale ? undefined : dataStreamStatList[0], - dataStreamDetails: isDataStreamStatStale ? undefined : dataStreamDetails, - dataStreamStatLoading, - dataStreamDetailsLoading, - fieldFormats, - }; - }, [ - type, - dataset, - namespace, - dataStreamStatList, - dataStreamStatLoading, + return { + dataStreamStat, dataStreamDetails, dataStreamDetailsLoading, fieldFormats, - ]); + }; }; - -function isStaleData(args: DataStreamNameParts, dataStreamStat?: DataStreamStatServiceResponse[0]) { - return ( - dataStreamStat && - dataStreamPartsToIndexName({ - type: dataStreamStat.type, - dataset: dataStreamStat.name, - namespace: dataStreamStat.namespace, - }) !== dataStreamPartsToIndexName(args) - ); -} diff --git a/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_table.tsx b/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_table.tsx index 5230a3e79cdf9..96f383f810b1b 100644 --- a/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_table.tsx +++ b/x-pack/plugins/dataset_quality/public/hooks/use_dataset_quality_table.tsx @@ -5,21 +5,21 @@ * 2.0. */ -import { useFetcher } from '@kbn/observability-shared-plugin/public'; -import { find, orderBy } from 'lodash'; -import React, { useCallback, useMemo, useState } from 'react'; +import { useSelector } from '@xstate/react'; +import { orderBy } from 'lodash'; +import React, { useCallback, useMemo } from 'react'; +import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../../common/constants'; import { DataStreamStat } from '../../common/data_streams_stats/data_stream_stat'; import { tableSummaryAllText, tableSummaryOfText } from '../../common/translations'; import { getDatasetQualityTableColumns } from '../components/dataset_quality/columns'; import { useDatasetQualityContext } from '../components/dataset_quality/context'; -import { getDefaultTimeRange, useKibanaContextForPlugin } from '../utils'; +import { FlyoutDataset } from '../state_machines/dataset_quality_controller'; +import { useKibanaContextForPlugin } from '../utils'; -const DEFAULT_SORT_FIELD = 'title'; -const DEFAULT_SORT_DIRECTION = 'desc'; -type DIRECTION = 'asc' | 'desc'; -type SORT_FIELD = keyof DataStreamStat; +export type Direction = 'asc' | 'desc'; +export type SortField = keyof DataStreamStat; -const sortingOverrides: Partial<{ [key in SORT_FIELD]: SORT_FIELD }> = { +const sortingOverrides: Partial<{ [key in SortField]: SortField }> = { ['title']: 'name', ['size']: 'sizeBytes', }; @@ -28,96 +28,114 @@ export const useDatasetQualityTable = () => { const { services: { fieldFormats }, } = useKibanaContextForPlugin(); - const [selectedDataset, setSelectedDataset] = useState(); - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); - const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); - const [sortDirection, setSortDirection] = useState(DEFAULT_SORT_DIRECTION); - - const defaultTimeRange = getDefaultTimeRange(); - - const { dataStreamsStatsServiceClient: client } = useDatasetQualityContext(); - const { data = [], loading } = useFetcher(async () => client.getDataStreamsStats(), []); - const { data: degradedStats = [], loading: loadingDegradedStats } = useFetcher( - async () => - client.getDataStreamsDegradedStats({ - start: defaultTimeRange.from, - end: defaultTimeRange.to, - }), - [] + + const { service } = useDatasetQualityContext(); + + const { page, rowsPerPage, sort } = useSelector(service, (state) => state.context.table); + + const flyout = useSelector(service, (state) => state.context.flyout); + + const loading = useSelector(service, (state) => state.matches('datasets.fetching')); + const loadingDegradedStats = useSelector(service, (state) => + state.matches('degradedDocs.fetching') + ); + + const datasets = useSelector(service, (state) => state.context.datasets); + + const isDatasetQualityPageIdle = useSelector(service, (state) => + state.matches('datasets.loaded.idle') + ); + + const closeFlyout = useCallback(() => service.send({ type: 'CLOSE_FLYOUT' }), [service]); + const openFlyout = useCallback( + (selectedDataset: FlyoutDataset) => { + if (flyout?.dataset?.rawName === selectedDataset.rawName) { + service.send({ + type: 'CLOSE_FLYOUT', + }); + + return; + } + + if (isDatasetQualityPageIdle) { + service.send({ + type: 'OPEN_FLYOUT', + dataset: selectedDataset, + }); + return; + } + + service.send({ + type: 'SELECT_NEW_DATASET', + dataset: selectedDataset, + }); + }, + [flyout?.dataset?.rawName, isDatasetQualityPageIdle, service] ); const columns = useMemo( () => getDatasetQualityTableColumns({ fieldFormats, - selectedDataset, - setSelectedDataset, + selectedDataset: flyout?.dataset, + openFlyout, loadingDegradedStats, }), - [fieldFormats, loadingDegradedStats, selectedDataset, setSelectedDataset] + [flyout?.dataset, fieldFormats, loadingDegradedStats, openFlyout] ); const pagination = { - pageIndex, - pageSize, - totalItemCount: data.length, + pageIndex: page, + pageSize: rowsPerPage, + totalItemCount: datasets.length, hidePerPageOptions: true, }; const onTableChange = useCallback( (options: { page: { index: number; size: number }; - sort?: { field: SORT_FIELD; direction: DIRECTION }; + sort?: { field: SortField; direction: Direction }; }) => { - setPageIndex(options.page.index); - setPageSize(options.page.size); - setSortField(options.sort?.field || DEFAULT_SORT_FIELD); - setSortDirection(options.sort?.direction || DEFAULT_SORT_DIRECTION); + service.send({ + type: 'UPDATE_TABLE_CRITERIA', + criteria: { + page: options.page.index, + rowsPerPage: options.page.size, + sort: { + field: options.sort?.field || DEFAULT_SORT_FIELD, + direction: options.sort?.direction || DEFAULT_SORT_DIRECTION, + }, + }, + }); }, - [] + [service] ); - const sort = { - sort: { field: sortField, direction: sortDirection }, - }; - const renderedItems = useMemo(() => { - const overridenSortingField = sortingOverrides[sortField] || sortField; - const mergedData = data.map((dataStream) => { - const degradedDocs = find(degradedStats, { dataset: dataStream.rawName }); - - return { - ...dataStream, - degradedDocs: degradedDocs?.percentage, - }; - }); + const overridenSortingField = sortingOverrides[sort.field] || sort.field; + const sortedItems = orderBy(datasets, overridenSortingField, sort.direction); - const sortedItems = orderBy(mergedData, overridenSortingField, sortDirection); - - return sortedItems.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); - }, [data, degradedStats, sortField, sortDirection, pageIndex, pageSize]); + return sortedItems.slice(page * rowsPerPage, (page + 1) * rowsPerPage); + }, [sort.field, sort.direction, datasets, page, rowsPerPage]); const resultsCount = useMemo(() => { - const startNumberItemsOnPage = pageSize * pageIndex + (renderedItems.length ? 1 : 0); - const endNumberItemsOnPage = pageSize * pageIndex + renderedItems.length; + const startNumberItemsOnPage = rowsPerPage ?? 1 * page ?? 0 + (renderedItems.length ? 1 : 0); + const endNumberItemsOnPage = rowsPerPage * page + renderedItems.length; - return pageSize === 0 ? ( + return rowsPerPage === 0 ? ( {tableSummaryAllText} ) : ( <> {startNumberItemsOnPage}-{endNumberItemsOnPage} {' '} - {tableSummaryOfText} {data.length} + {tableSummaryOfText} {datasets.length} ); - }, [data.length, pageIndex, pageSize, renderedItems.length]); - - const closeFlyout = useCallback(() => setSelectedDataset(undefined), []); + }, [rowsPerPage, page, renderedItems.length, datasets.length]); return { - sort, + sort: { sort }, onTableChange, pagination, renderedItems, @@ -125,6 +143,6 @@ export const useDatasetQualityTable = () => { loading, resultsCount, closeFlyout, - selectedDataset, + selectedDataset: flyout?.dataset, }; }; diff --git a/x-pack/plugins/dataset_quality/public/hooks/use_link_to_log_explorer.ts b/x-pack/plugins/dataset_quality/public/hooks/use_link_to_log_explorer.ts index ee019cb75c7ba..e9f279c1eaecc 100644 --- a/x-pack/plugins/dataset_quality/public/hooks/use_link_to_log_explorer.ts +++ b/x-pack/plugins/dataset_quality/public/hooks/use_link_to_log_explorer.ts @@ -11,9 +11,14 @@ import { } from '@kbn/deeplinks-observability'; import { getRouterLinkProps } from '@kbn/router-utils'; import { DataStreamStat } from '../../common/data_streams_stats/data_stream_stat'; +import { FlyoutDataset } from '../state_machines/dataset_quality_controller'; import { useKibanaContextForPlugin } from '../utils'; -export const useLinkToLogExplorer = ({ dataStreamStat }: { dataStreamStat: DataStreamStat }) => { +export const useLinkToLogExplorer = ({ + dataStreamStat, +}: { + dataStreamStat: DataStreamStat | FlyoutDataset; +}) => { const { services: { share }, } = useKibanaContextForPlugin(); diff --git a/x-pack/plugins/dataset_quality/public/plugin.tsx b/x-pack/plugins/dataset_quality/public/plugin.tsx index c2ab655422631..40c71c9faaf87 100644 --- a/x-pack/plugins/dataset_quality/public/plugin.tsx +++ b/x-pack/plugins/dataset_quality/public/plugin.tsx @@ -7,6 +7,7 @@ import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import { createDatasetQuality } from './components/dataset_quality'; +import { createDatasetQualityControllerLazyFactory } from './controller/lazy_create_controller'; import { DatasetQualityPluginSetup, DatasetQualityPluginStart, @@ -29,6 +30,11 @@ export class DatasetQualityPlugin plugins, }); - return { DatasetQuality }; + const createDatasetQualityController = createDatasetQualityControllerLazyFactory({ + core, + plugins, + }); + + return { DatasetQuality, createDatasetQualityController }; } } diff --git a/x-pack/plugins/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts b/x-pack/plugins/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts index 2a4041934c7bf..28fba012492f4 100644 --- a/x-pack/plugins/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts +++ b/x-pack/plugins/dataset_quality/public/services/data_streams_stats/data_streams_stats_client.ts @@ -39,13 +39,13 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { query: params, }) .catch((error) => { - throw new GetDataStreamsStatsError(`Failed to fetch data streams stats": ${error}`); + throw new GetDataStreamsStatsError(`Failed to fetch data streams stats: ${error}`); }); const { dataStreamsStats, integrations } = decodeOrThrow( getDataStreamsStatsResponseRt, (message: string) => - new GetDataStreamsStatsError(`Failed to decode data streams stats response: ${message}"`) + new GetDataStreamsStatsError(`Failed to decode data streams stats response: ${message}`) )(response); const mergedDataStreamsStats = dataStreamsStats.map((statsItem) => { @@ -69,16 +69,14 @@ export class DataStreamsStatsClient implements IDataStreamsStatsClient { } ) .catch((error) => { - throw new GetDataStreamsStatsError( - `Failed to fetch data streams degraded stats": ${error}` - ); + throw new GetDataStreamsStatsError(`Failed to fetch data streams degraded stats: ${error}`); }); const { degradedDocs } = decodeOrThrow( getDataStreamsDegradedDocsStatsResponseRt, (message: string) => new GetDataStreamsStatsError( - `Failed to decode data streams degraded docs stats response: ${message}"` + `Failed to decode data streams degraded docs stats response: ${message}` ) )(response); diff --git a/x-pack/plugins/dataset_quality/public/services/data_streams_stats/types.ts b/x-pack/plugins/dataset_quality/public/services/data_streams_stats/types.ts index 26490f0a0bf43..0a70dc1d86489 100644 --- a/x-pack/plugins/dataset_quality/public/services/data_streams_stats/types.ts +++ b/x-pack/plugins/dataset_quality/public/services/data_streams_stats/types.ts @@ -12,8 +12,8 @@ import { GetDataStreamsDegradedDocsStatsQuery, GetDataStreamsStatsQuery, GetDataStreamDetailsParams, + DataStreamDetails, } from '../../../common/data_streams_stats'; -import { DataStreamDetails } from '../../../common/data_streams_stats/data_stream_details'; export type DataStreamsStatsServiceSetup = void; diff --git a/x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_details.ts b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/index.ts similarity index 80% rename from x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_details.ts rename to x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/index.ts index 3d4dc00905ec1..3b2a320ae181f 100644 --- a/x-pack/plugins/dataset_quality/common/data_streams_stats/data_stream_details.ts +++ b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/index.ts @@ -5,6 +5,4 @@ * 2.0. */ -export interface DataStreamDetails { - createdOn?: number; -} +export * from './src'; diff --git a/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/defaults.ts b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/defaults.ts new file mode 100644 index 0000000000000..ecd044727f610 --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/defaults.ts @@ -0,0 +1,22 @@ +/* + * 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_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../../../../common/constants'; +import { DefaultDatasetQualityControllerState } from './types'; + +export const DEFAULT_CONTEXT: DefaultDatasetQualityControllerState = { + table: { + page: 0, + rowsPerPage: 10, + sort: { + field: DEFAULT_SORT_FIELD, + direction: DEFAULT_SORT_DIRECTION, + }, + }, + flyout: {}, + datasets: [], +}; diff --git a/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/index.ts b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/index.ts new file mode 100644 index 0000000000000..74e05d6ca2114 --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; 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'; diff --git a/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.ts new file mode 100644 index 0000000000000..c3cb600951463 --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/notifications.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 { IToasts } from '@kbn/core/public'; +import { i18n } from '@kbn/i18n'; + +export const fetchDatasetStatsFailedNotifier = (toasts: IToasts, error: Error) => { + toasts.addDanger({ + title: i18n.translate('xpack.datasetQuality.fetchDatasetStatsFailed', { + defaultMessage: "We couldn't get your datasets.", + }), + text: error.message, + }); +}; + +export const fetchDatasetDetailsFailedNotifier = (toasts: IToasts, error: Error) => { + toasts.addDanger({ + title: i18n.translate('xpack.datasetQuality.fetchDatasetDetailsFailed', { + defaultMessage: "We couldn't get your dataset details.", + }), + text: error.message, + }); +}; + +export const fetchDegradedStatsFailedNotifier = (toasts: IToasts, error: Error) => { + toasts.addDanger({ + title: i18n.translate('xpack.datasetQuality.fetchDegradedStatsFailed', { + defaultMessage: "We couldn't get your degraded docs information.", + }), + text: error.message, + }); +}; + +export const noDatasetSelected = i18n.translate( + 'xpack.datasetQuality.fetchDatasetDetailsFailed.noDatasetSelected', + { + defaultMessage: 'No dataset have been selected', + } +); diff --git a/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts new file mode 100644 index 0000000000000..b2222ea3c0d02 --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/state_machine.ts @@ -0,0 +1,254 @@ +/* + * 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/public'; +import { assign, createMachine, DoneInvokeEvent, InterpreterFrom } from 'xstate'; +import { mergeDegradedStatsIntoDataStreams } from '../../../utils/merge_degraded_docs_into_datastreams'; +import { DataStreamDetails } from '../../../../common/data_streams_stats'; +import { DataStreamType } from '../../../../common/types'; +import { dataStreamPartsToIndexName } from '../../../../common/utils'; +import { DataStreamStat } from '../../../../common/data_streams_stats/data_stream_stat'; +import { getDefaultTimeRange } from '../../../utils'; +import { IDataStreamsStatsClient } from '../../../services/data_streams_stats'; +import { DEFAULT_CONTEXT } from './defaults'; +import { + DatasetQualityControllerContext, + DatasetQualityControllerEvent, + DatasetQualityControllerTypeState, + FlyoutDataset, +} from './types'; +import { DegradedDocsStat } from '../../../../common/data_streams_stats/malformed_docs_stat'; +import { + fetchDatasetDetailsFailedNotifier, + fetchDatasetStatsFailedNotifier, + fetchDegradedStatsFailedNotifier, + noDatasetSelected, +} from './notifications'; + +export const createPureDatasetQualityControllerStateMachine = ( + initialContext: DatasetQualityControllerContext +) => + /** @xstate-layout N4IgpgJg5mDOIC5QBECGAXVszoIoFdUAbAS3QE8BhAewDt0AnaoosBgOggyx1nYDMcAYwAWJWlADEEOmHbiAbtQDWctJmx5CpCjXpMWbTt019B6UeKgJF1IRhJ0A2gAYAuq7eJQAB2qwyR1pvEAAPRABaADYAVnYADgBORIBGABZUtIBmLPiUgCY0gBoQckj8xPZ8lJi0lPiXKKi0-KyXeLSAX06S9R4tYjIqOkZmVg4uDV4BYTEJSTYmDh8iDH5qBgBbdj7NAkHdEYNx4yn0M1mrG1ole3Qgz08QvwD7uhDwhGSAdnYYlMSMXiMXy31iUWBJTKCAiNXY3xiURcaTSLhSLm+aW+yUS3V6Jhw+x0w30YyMk36fCI1FQEEgkgAqgAFZAAQQAKgBRAD67NZACEADI8ygAJQAklyJaynkgQC9Au85Z8QfF2C1WqksmDvoV8lDIgDfvF4lFvjVWkkIfEsniQLtCdohnpRoYJmAoAxaZBkHYLhY5lIZLQ5LZVDsCQNiS7juSPV66RBfUJ-ZYJNdbg5nO5Zb5-IrgsrEOj8nFvmj2vl8i4ay59aVEK18n9vib2jWoolvrlvnaHVHnUcye7Pd6k36ZgGrAsGEt2Cs1httv2iYPSW7OPGx8nU4GM3Ys7RHu5nvm3oXQJ8Irr2C5Ema2siWlEsqCDQhClFb7lq6iYveXxNPtI1XQ51xOfgiHIah8HQSQAGVOWFSh2W5AA5TkAHVuTZPlEPZXN5TPIIPkQNIGgSRIshqZpTRcHJigbBBETVeJdTqZpzUA+JgLOUCSVdCCoJguDKEFAB5RDuQAMUFABNcSGQIk85QVc9SJhbsqiiaoCiyFEOl098mkqRp4mrJIUiabtex6e0QKdMDBKMSDoNgyc0yDWR5BuFQ1Acg4BNjDhXJEjy91sO4HhzFS81eEiiwQG1KmxOiXBibVGkSeJ311ZsITSGIYnS2IawyXj+n4mNhwEYT3PMTyZznBd0HWLYIz4xygpq0L6sudNIsPY8vFU4ilUvRA8hSW8WkBU0qIBdp3yBSpgXLcsmnaRJyrslcuuqjdevQdhqTHSQxMknlZIUpTCLUhKJo-WJ4VNVsUlfF9snyKJ3zWv42MK5JURffIKr2fah0OurjqEalsAgSRxKZTk0Jk+TFOUka4oLDTQWm9KCnRO9iuqH6mM7L8wTycs0U7ZJbTtWhqDpeA5T2wKDvGU94vGsJDU7bTdNaAyWhSd8Im1fKUgRb5dRrbKslxXaAujSGTgpUxuZxxLYXeyjMkyHI8kKcX8mBKoQRqSyzSieowcdDm1fJSNdysLX1MSkE0nYLtEjNztajSWJ62hCJWL0tikiK4q2PtgcnOC05KROmlE3dh6+YQcjm1bJEzYRdo8kScWu3YaiwWo+pOPaOOqqdkcEx9P1095q8ChcW9727Mrn1fb533SOJqhyZpX1SBolfxTrHfAuNR0THdwrd0aeYvTOg+9rFqkBQp6JyfumMKLJ4TaaXQSRGXGlriHZ4b7cJ1OtOV+1x7A4SNobVbIEaJDxBtUqb6LQkg2gqI0BmU9Ko32ciFaGLc15t2qILFIekRZGSYuRaaxVyzVGRFZNoXRlbT1VrfWqbljoNUDHAjSWIvwFEBmbLInFZYxGMoCH2n1qxVgBBPa+M9oGkLCo-SAVDErIPIuqcyvs6xsVLGTaE3Zppf3-M0LsR9bIQPBnwxOR12Cw38MI5+HtHrpF+JxEmFRpZvRYUxc0lRFZ1G1HUdEKRujdCAA */ + createMachine< + DatasetQualityControllerContext, + DatasetQualityControllerEvent, + DatasetQualityControllerTypeState + >( + { + context: initialContext, + predictableActionArguments: true, + id: 'DatasetQualityController', + type: 'parallel', + states: { + datasets: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'loadDataStreamStats', + onDone: { + target: 'loaded', + actions: ['storeDataStreamStats', 'storeDatasets'], + }, + onError: { + target: 'loaded', + actions: ['notifyFetchDatasetStatsFailed'], + }, + }, + }, + loaded: { + on: { + UPDATE_TABLE_CRITERIA: { + target: 'loaded', + actions: ['storeTableOptions'], + }, + }, + }, + }, + }, + degradedDocs: { + initial: 'fetching', + states: { + fetching: { + invoke: { + src: 'loadDegradedDocs', + onDone: { + target: 'loaded', + actions: ['storeDegradedDocStats', 'storeDatasets'], + }, + onError: { + target: 'loaded', + actions: ['notifyFetchDegradedStatsFailed'], + }, + }, + }, + loaded: {}, + }, + }, + flyout: { + initial: 'closed', + states: { + fetching: { + invoke: { + src: 'loadDataStreamDetails', + onDone: { + target: 'loaded', + actions: ['storeDatasetDetails'], + }, + onError: { + target: 'loaded', + actions: ['fetchDatasetDetailsFailedNotifier'], + }, + }, + }, + loaded: { + on: { + CLOSE_FLYOUT: { + target: 'closed', + actions: ['resetFlyoutOptions'], + }, + }, + }, + closed: { + on: { + OPEN_FLYOUT: { + target: '#DatasetQualityController.flyout.fetching', + actions: ['storeFlyoutOptions'], + }, + }, + }, + }, + on: { + SELECT_NEW_DATASET: { + target: '#DatasetQualityController.flyout.fetching', + actions: ['storeFlyoutOptions'], + }, + CLOSE_FLYOUT: { + target: '#DatasetQualityController.flyout.closed', + actions: ['resetFlyoutOptions'], + }, + }, + }, + }, + }, + { + actions: { + storeTableOptions: assign((_context, event) => { + return 'criteria' in event + ? { + table: event.criteria, + } + : {}; + }), + storeFlyoutOptions: assign((context, event) => { + return 'dataset' in event + ? { + flyout: { + ...context.flyout, + dataset: event.dataset as FlyoutDataset, + }, + } + : {}; + }), + resetFlyoutOptions: assign((_context, _event) => ({ flyout: undefined })), + storeDataStreamStats: assign((_context, event) => { + return 'data' in event + ? { + dataStreamStats: event.data as DataStreamStat[], + } + : {}; + }), + storeDegradedDocStats: assign((_context, event) => { + return 'data' in event + ? { + degradedDocStats: event.data as DegradedDocsStat[], + } + : {}; + }), + storeDatasetDetails: assign((context, event) => { + return 'data' in event + ? { + flyout: { + ...context.flyout, + datasetDetails: event.data as DataStreamDetails, + }, + } + : {}; + }), + storeDatasets: assign((context, _event) => { + return context.dataStreamStats && context.degradedDocStats + ? { + datasets: mergeDegradedStatsIntoDataStreams( + context.dataStreamStats, + context.degradedDocStats + ), + } + : context.dataStreamStats + ? { datasets: context.dataStreamStats } + : { datasets: [] }; + }), + }, + } + ); + +export interface DatasetQualityControllerStateMachineDependencies { + initialContext?: DatasetQualityControllerContext; + toasts: IToasts; + dataStreamStatsClient: IDataStreamsStatsClient; +} + +export const createDatasetQualityControllerStateMachine = ({ + initialContext = DEFAULT_CONTEXT, + toasts, + dataStreamStatsClient, +}: DatasetQualityControllerStateMachineDependencies) => + createPureDatasetQualityControllerStateMachine(initialContext).withConfig({ + actions: { + notifyFetchDatasetStatsFailed: (_context, event: DoneInvokeEvent) => + fetchDatasetStatsFailedNotifier(toasts, event.data), + notifyFetchDegradedStatsFailed: (_context, event: DoneInvokeEvent) => + fetchDegradedStatsFailedNotifier(toasts, event.data), + notifyFetchDatasetDetailsFailed: (_context, event: DoneInvokeEvent) => + fetchDatasetDetailsFailedNotifier(toasts, event.data), + }, + services: { + loadDataStreamStats: (_context) => dataStreamStatsClient.getDataStreamsStats(), + loadDegradedDocs: (_context) => { + const defaultTimeRange = getDefaultTimeRange(); + + return dataStreamStatsClient.getDataStreamsDegradedStats({ + start: defaultTimeRange.from, + end: defaultTimeRange.to, + }); + }, + loadDataStreamDetails: (context) => { + if (!context.flyout.dataset) { + fetchDatasetDetailsFailedNotifier(toasts, new Error(noDatasetSelected)); + + return Promise.resolve({}); + } + + const { type, name: dataset, namespace } = context.flyout.dataset; + + return dataStreamStatsClient.getDataStreamDetails({ + dataStream: dataStreamPartsToIndexName({ + type: type as DataStreamType, + dataset, + namespace, + }), + }); + }, + }, + }); + +export type DatasetQualityControllerStateService = InterpreterFrom< + typeof createDatasetQualityControllerStateMachine +>; + +export type DatasetQualityControllerStateMachine = ReturnType< + typeof createDatasetQualityControllerStateMachine +>; diff --git a/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts new file mode 100644 index 0000000000000..4e059bd9dbeaa --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/state_machines/dataset_quality_controller/src/types.ts @@ -0,0 +1,122 @@ +/* + * 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 { DoneInvokeEvent } from 'xstate'; +import { Direction, SortField } from '../../../hooks'; +import { DegradedDocsStat } from '../../../../common/data_streams_stats/malformed_docs_stat'; +import { + DataStreamDegradedDocsStatServiceResponse, + DataStreamDetails, + DataStreamStatServiceResponse, +} from '../../../../common/data_streams_stats'; +import { DataStreamStat } from '../../../../common/data_streams_stats/data_stream_stat'; + +export interface FlyoutDataset { + rawName: string; + type: string; + name: string; + namespace: string; + title: string; + integration?: { + name: string; + title: string; + version: string; + }; +} + +interface TableCriteria { + page: number; + rowsPerPage: number; + sort: { + field: SortField; + direction: Direction; + }; +} + +export interface WithTableOptions { + table: TableCriteria; +} + +export interface WithFlyoutOptions { + flyout: { + dataset?: FlyoutDataset; + datasetDetails?: DataStreamDetails; + }; +} + +export interface WithDataStreamStats { + dataStreamStats: DataStreamStat[]; +} + +export interface WithDegradedDocs { + degradedDocStats: DegradedDocsStat[]; +} + +export interface WithDatasets { + datasets: DataStreamStat[]; +} + +export type DefaultDatasetQualityControllerState = WithTableOptions & + Partial & + Partial & + WithFlyoutOptions & + WithDatasets; + +type DefaultDatasetQualityStateContext = DefaultDatasetQualityControllerState & + Partial; + +export type DatasetQualityControllerTypeState = + | { + value: 'datasets.fetching'; + context: DefaultDatasetQualityStateContext; + } + | { + value: 'datasets.loaded'; + context: DefaultDatasetQualityStateContext; + } + | { + value: 'datasets.loaded.idle'; + context: DefaultDatasetQualityStateContext; + } + | { + value: 'datasets.loaded.flyoutOpen.fetching'; + context: DefaultDatasetQualityStateContext; + } + | { + value: 'datasets.loaded.flyoutOpen'; + context: DefaultDatasetQualityStateContext; + } + | { + value: 'degradedDocs.fetching'; + context: DefaultDatasetQualityStateContext; + } + | { + value: 'datasets.loaded'; + context: DefaultDatasetQualityStateContext; + }; + +export type DatasetQualityControllerContext = DatasetQualityControllerTypeState['context']; + +export type DatasetQualityControllerEvent = + | { + type: 'UPDATE_TABLE_CRITERIA'; + criteria: TableCriteria; + } + | { + type: 'OPEN_FLYOUT'; + dataset: FlyoutDataset; + } + | { + type: 'SELECT_NEW_DATASET'; + dataset: FlyoutDataset; + } + | { + type: 'CLOSE_FLYOUT'; + } + | DoneInvokeEvent + | DoneInvokeEvent + | DoneInvokeEvent; diff --git a/x-pack/plugins/dataset_quality/public/types.ts b/x-pack/plugins/dataset_quality/public/types.ts index 482aff3b242b9..e8df65407e41e 100644 --- a/x-pack/plugins/dataset_quality/public/types.ts +++ b/x-pack/plugins/dataset_quality/public/types.ts @@ -8,12 +8,15 @@ import { ComponentType } from 'react'; import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; +import { CreateDatasetQualityController } from './controller'; +import { DatasetQualityProps } from './components/dataset_quality'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface DatasetQualityPluginSetup {} export interface DatasetQualityPluginStart { - DatasetQuality: ComponentType; + DatasetQuality: ComponentType; + createDatasetQualityController: CreateDatasetQualityController; } export interface DatasetQualityStartDeps { diff --git a/x-pack/plugins/dataset_quality/public/utils/merge_degraded_docs_into_datastreams.ts b/x-pack/plugins/dataset_quality/public/utils/merge_degraded_docs_into_datastreams.ts new file mode 100644 index 0000000000000..e51309f413647 --- /dev/null +++ b/x-pack/plugins/dataset_quality/public/utils/merge_degraded_docs_into_datastreams.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DataStreamStat } from '../../common/data_streams_stats'; +import { DegradedDocsStat } from '../../common/data_streams_stats/malformed_docs_stat'; + +export function mergeDegradedStatsIntoDataStreams( + dataStreamStats: DataStreamStat[], + degradedDocStats: DegradedDocsStat[] +) { + const degradedMap: Record = + degradedDocStats.reduce( + (degradedMapAcc, { dataset, percentage }) => + Object.assign(degradedMapAcc, { [dataset]: percentage }), + {} + ); + + return dataStreamStats?.map((dataStream) => ({ + ...dataStream, + degradedDocs: degradedMap[dataStream.name], + })); +} diff --git a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_stream_details/index.ts b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_stream_details/index.ts index 5d122df09398f..65fa54babcc30 100644 --- a/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_stream_details/index.ts +++ b/x-pack/plugins/dataset_quality/server/routes/data_streams/get_data_stream_details/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { badRequest } from '@hapi/boom'; import type { ElasticsearchClient } from '@kbn/core/server'; import { DataStreamDetails } from '../../../../common/api_types'; import { dataStreamService } from '../../../services'; @@ -16,15 +17,12 @@ export async function getDataStreamDetails(args: { const { esClient, dataStream } = args; if (!dataStream?.trim()) { - throw new Error(`Data Stream name cannot be empty. Received value "${dataStream}"`); + throw badRequest(`Data Stream name cannot be empty. Received value "${dataStream}"`); } const indexSettings = await dataStreamService.getDataSteamIndexSettings(esClient, dataStream); const indexesList = Object.values(indexSettings); - if (indexesList.length < 1) { - throw new Error('index_not_found_exception'); - } const indexCreationDate = indexesList .map((index) => Number(index.settings?.index?.creation_date)) diff --git a/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts b/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts index 340c957f6e41a..02f1508433e79 100644 --- a/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts +++ b/x-pack/plugins/dataset_quality/server/routes/data_streams/routes.ts @@ -7,7 +7,7 @@ import * as t from 'io-ts'; import { keyBy, merge, values } from 'lodash'; -import Boom from '@hapi/boom'; +import { DataStreamType } from '../../../common/types'; import { DataStreamDetails, DataStreamStat, @@ -117,17 +117,21 @@ const dataStreamDetailsRoute = createDatasetQualityServerRoute({ // Query datastreams as the current user as the Kibana internal user may not have all the required permissions const esClient = coreContext.elasticsearch.client.asCurrentUser; - try { - return await getDataStreamDetails({ esClient, dataStream }); - } catch (e) { - if (e) { - if (e?.message?.indexOf('index_not_found_exception') > -1) { - throw Boom.notFound(`Data stream "${dataStream}" not found.`); - } - } - - throw e; - } + const [type, ...datasetQuery] = dataStream.split('-'); + + const [dataStreamsStats, dataStreamDetails] = await Promise.all([ + getDataStreamsStats({ + esClient, + type: type as DataStreamType, + datasetQuery: datasetQuery.join('-'), + }), + getDataStreamDetails({ esClient, dataStream }), + ]); + + return { + createdOn: dataStreamDetails?.createdOn, + lastActivity: dataStreamsStats.items?.[0]?.lastActivity, + }; }, }); diff --git a/x-pack/plugins/dataset_quality/server/services/data_stream.ts b/x-pack/plugins/dataset_quality/server/services/data_stream.ts index c8e675548354e..9bd93efc9341c 100644 --- a/x-pack/plugins/dataset_quality/server/services/data_stream.ts +++ b/x-pack/plugins/dataset_quality/server/services/data_stream.ts @@ -67,11 +67,18 @@ class DataStreamService { esClient: ElasticsearchClient, dataStream: string ): Promise>> { - const settings = await esClient.indices.getSettings({ - index: dataStream, - }); + try { + const settings = await esClient.indices.getSettings({ + index: dataStream, + }); - return settings; + return settings; + } catch (e) { + if (e.statusCode === 404) { + return {}; + } + throw e; + } } } diff --git a/x-pack/plugins/dataset_quality/tsconfig.json b/x-pack/plugins/dataset_quality/tsconfig.json index 65f77bac100aa..be630d1b664ab 100644 --- a/x-pack/plugins/dataset_quality/tsconfig.json +++ b/x-pack/plugins/dataset_quality/tsconfig.json @@ -14,7 +14,6 @@ "@kbn/core-plugins-server", "@kbn/core-elasticsearch-server-mocks", "@kbn/fleet-plugin", - "@kbn/observability-shared-plugin", "@kbn/server-route-repository", "@kbn/share-plugin", "@kbn/std", @@ -28,6 +27,8 @@ "@kbn/es-types", "@kbn/deeplinks-observability", "@kbn/router-utils", + "@kbn/xstate-utils", + "@kbn/shared-ux-utility", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/common/index.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/common/index.ts index 5ef2ca71e5ce1..42dd55a353dc1 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/common/index.ts @@ -10,5 +10,10 @@ export { SingleDatasetLocatorDefinition, AllDatasetsLocatorDefinition, } from './locators'; -export { OBSERVABILITY_LOGS_EXPLORER_URL_STATE_KEY, urlSchemaV1 } from './url_schema'; +export { + OBSERVABILITY_LOGS_EXPLORER_URL_STATE_KEY, + OBSERVABILITY_DATASET_QUALITY_URL_STATE_KEY, + logExplorerUrlSchemaV1, + datasetQualityUrlSchemaV1, +} from './url_schema'; export { deepCompactObject } from './utils/deep_compact_object'; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/utils/construct_locator_path.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/utils/construct_locator_path.ts index d2464184107f2..c809068c31f94 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/utils/construct_locator_path.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/common/locators/utils/construct_locator_path.ts @@ -17,10 +17,13 @@ import { DatasetSelectionPlain, } from '@kbn/logs-explorer-plugin/common'; import { OBSERVABILITY_LOGS_EXPLORER_APP_ID } from '@kbn/deeplinks-observability'; -import { OBSERVABILITY_LOGS_EXPLORER_URL_STATE_KEY, urlSchemaV1 } from '../../url_schema'; +import { + OBSERVABILITY_LOGS_EXPLORER_URL_STATE_KEY, + logExplorerUrlSchemaV1, +} from '../../url_schema'; import { deepCompactObject } from '../../utils/deep_compact_object'; -type ControlsPageState = NonNullable; +type ControlsPageState = NonNullable; interface LocatorPathConstructionParams { datasetSelection: DatasetSelectionPlain; @@ -35,7 +38,7 @@ export const constructLocatorPath = async (params: LocatorPathConstructionParams useHash, } = params; - const pageState = urlSchemaV1.urlSchemaRT.encode( + const pageState = logExplorerUrlSchemaV1.urlSchemaRT.encode( deepCompactObject({ v: 1, datasetSelection, diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/common.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/common.ts index 22b00ded2e180..f1fd80cbb9b5e 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/common.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/common.ts @@ -6,3 +6,4 @@ */ export const OBSERVABILITY_LOGS_EXPLORER_URL_STATE_KEY = 'pageState'; +export const OBSERVABILITY_DATASET_QUALITY_URL_STATE_KEY = 'pageState'; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/dataset_quality/url_schema_v1.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/dataset_quality/url_schema_v1.ts new file mode 100644 index 0000000000000..f8afdec14f23a --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/dataset_quality/url_schema_v1.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 * 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, + }) +); + +const integrationRT = rt.strict({ + name: rt.string, + title: rt.string, + version: rt.string, +}); + +const datasetRT = rt.intersection([ + rt.strict({ + rawName: rt.string, + type: rt.string, + name: rt.string, + namespace: rt.string, + title: rt.string, + }), + rt.exact( + rt.partial({ + integration: integrationRT, + }) + ), +]); + +export const flyoutRT = rt.exact( + rt.partial({ + dataset: datasetRT, + }) +); + +export const urlSchemaRT = rt.exact( + rt.partial({ + v: rt.literal(1), + table: tableRT, + flyout: flyoutRT, + }) +); + +export type UrlSchema = rt.TypeOf; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/index.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/index.ts index f0873d9949426..def188f947c7d 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/index.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/common/url_schema/index.ts @@ -5,5 +5,9 @@ * 2.0. */ -export { OBSERVABILITY_LOGS_EXPLORER_URL_STATE_KEY } from './common'; -export * as urlSchemaV1 from './url_schema_v1'; +export { + OBSERVABILITY_LOGS_EXPLORER_URL_STATE_KEY, + OBSERVABILITY_DATASET_QUALITY_URL_STATE_KEY, +} from './common'; +export * as logExplorerUrlSchemaV1 from './url_schema_v1'; +export * as datasetQualityUrlSchemaV1 from './dataset_quality/url_schema_v1'; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_log_explorer.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_log_explorer.tsx index a823ad1a840cd..547c9771958eb 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_log_explorer.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/applications/observability_log_explorer.tsx @@ -71,11 +71,7 @@ export const ObservabilityLogExplorerApp = ({ } /> - } - /> + } /> diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/routes/main/dataset_quality_route.tsx b/x-pack/plugins/observability_solution/observability_logs_explorer/public/routes/main/dataset_quality_route.tsx index a58df53c7b1a7..980e6b3193892 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/routes/main/dataset_quality_route.tsx +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/routes/main/dataset_quality_route.tsx @@ -5,32 +5,104 @@ * 2.0. */ +import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; import { CoreStart } from '@kbn/core/public'; +import { DatasetQualityPluginStart } from '@kbn/dataset-quality-plugin/public'; +import { DatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller'; +import { OBSERVABILITY_LOGS_EXPLORER_APP_ID } from '@kbn/deeplinks-observability'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useLinkProps } from '@kbn/observability-shared-plugin/public'; +import { useActor } from '@xstate/react'; import React from 'react'; -import { EuiBreadcrumb } from '@elastic/eui'; -import { datasetQualityAppTitle } from '@kbn/dataset-quality-plugin/public'; import { ObservabilityLogExplorerPageTemplate } from '../../components/page_template'; +import { + ObservabilityDatasetQualityPageStateProvider, + useObservabilityDatasetQualityPageStateContext, +} from '../../state_machines/dataset_quality/src/provider'; import { useBreadcrumbs } from '../../utils/breadcrumbs'; +import { useKbnUrlStateStorageFromRouterContext } from '../../utils/kbn_url_state_context'; import { useKibanaContextForPlugin } from '../../utils/use_kibana'; export interface DatasetQualityRouteProps { core: CoreStart; } -export const DatasetQualityRoute = ({ core }: DatasetQualityRouteProps) => { +export const DatasetQualityRoute = () => { const { services } = useKibanaContextForPlugin(); - const { serverless, datasetQuality: DatasetQuality } = services; - const breadcrumb: EuiBreadcrumb[] = [ - { - text: datasetQualityAppTitle, - }, - ]; + const { datasetQuality, serverless, chrome, notifications } = services; + const logExplorerLinkProps = useLinkProps({ app: OBSERVABILITY_LOGS_EXPLORER_APP_ID }); - useBreadcrumbs(breadcrumb, core.chrome, serverless); + useBreadcrumbs( + [ + { + text: 'Datasets', + ...logExplorerLinkProps, + }, + ], + chrome, + serverless + ); + + const urlStateStorageContainer = useKbnUrlStateStorageFromRouterContext(); return ( - - - + + datasetQuality.createDatasetQualityController(initialState) + } + toasts={notifications.toasts} + urlStateStorageContainer={urlStateStorageContainer} + > + + ); }; + +const ConnectedContent = React.memo(() => { + const { + services: { datasetQuality }, + } = useKibanaContextForPlugin(); + + const [state] = useActor(useObservabilityDatasetQualityPageStateContext()); + + if (state.matches('initialized')) { + return ( + + ); + } else { + return ; + } +}); + +const InitializingContent = React.memo(() => ( + + } + title={ + + } + /> + +)); + +const InitializedContent = React.memo( + ({ + datasetQuality, + datasetQualityController, + }: { + datasetQuality: DatasetQualityPluginStart; + datasetQualityController: DatasetQualityController; + }) => { + return ( + + + + ); + } +); diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/controller_service.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/controller_service.ts new file mode 100644 index 0000000000000..9847b24fcd03c --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/controller_service.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CreateDatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller'; +import type { InvokeCreator } from 'xstate'; +import type { ObservabilityDatasetQualityContext, ObservabilityDatasetQualityEvent } from './types'; + +export const createController = + ({ + createDatasetQualityController, + }: { + createDatasetQualityController: CreateDatasetQualityController; + }): InvokeCreator => + (context, event) => + (send) => { + createDatasetQualityController({ + initialState: context.initialDatasetQualityState, + }).then((controller) => { + send({ + type: 'CONTROLLER_CREATED', + controller, + }); + }); + }; + +export const subscribeToDatasetQualityState: InvokeCreator< + ObservabilityDatasetQualityContext, + ObservabilityDatasetQualityEvent +> = (context, _event) => (send) => { + if (!('controller' in context)) { + throw new Error('Failed to subscribe to controller: no controller in context'); + } + + const { controller } = context; + + const subscription = controller.state$.subscribe({ + next: (state) => { + send({ type: 'DATASET_QUALITY_STATE_CHANGED', state }); + }, + }); + + controller.service.start(); + + return () => { + subscription.unsubscribe(); + controller.service.stop(); + }; +}; + +export const openDatasetFlyout = (context: ObservabilityDatasetQualityContext) => { + if (!('controller' in context)) { + throw new Error('Failed to subscribe to controller: no controller in context'); + } + + const { + controller, + initialDatasetQualityState: { flyout }, + } = context; + + if (flyout?.dataset) { + controller.service.send({ + type: 'OPEN_FLYOUT', + dataset: flyout.dataset, + }); + } +}; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/defaults.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/defaults.ts new file mode 100644 index 0000000000000..364049ad3f1c2 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/defaults.ts @@ -0,0 +1,12 @@ +/* + * 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 { CommonObservabilityDatasetQualityContext } from './types'; + +export const DEFAULT_CONTEXT: CommonObservabilityDatasetQualityContext = { + initialDatasetQualityState: {}, +}; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/provider.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/provider.ts new file mode 100644 index 0000000000000..e900e4851cfc3 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/provider.ts @@ -0,0 +1,30 @@ +/* + * 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 { getDevToolsOptions } from '@kbn/xstate-utils'; +import { useInterpret } from '@xstate/react'; +import createContainer from 'constate'; +import { + createObservabilityDatasetQualityStateMachine, + ObservabilityDatasetQualityStateMachineDependencies, +} from './state_machine'; + +export const useObservabilityDatasetQualityPageState = ( + deps: ObservabilityDatasetQualityStateMachineDependencies +) => { + const observabilityDatasetQualityPageStateService = useInterpret( + () => createObservabilityDatasetQualityStateMachine(deps), + { devTools: getDevToolsOptions() } + ); + + return observabilityDatasetQualityPageStateService; +}; + +export const [ + ObservabilityDatasetQualityPageStateProvider, + useObservabilityDatasetQualityPageStateContext, +] = createContainer(useObservabilityDatasetQualityPageState); diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/state_machine.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/state_machine.ts new file mode 100644 index 0000000000000..71e97192e07ec --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/state_machine.ts @@ -0,0 +1,146 @@ +/* + * 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 { CreateDatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller'; +import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public'; +import { actions, createMachine, InterpreterFrom } from 'xstate'; +import { + createController, + openDatasetFlyout, + subscribeToDatasetQualityState, +} from './controller_service'; +import { DEFAULT_CONTEXT } from './defaults'; +import { + ObservabilityDatasetQualityContext, + ObservabilityDatasetQualityEvent, + ObservabilityDatasetQualityTypeState, +} from './types'; +import { initializeFromUrl, updateUrlFromDatasetQualityState } from './url_state_storage_service'; + +export const createPureObservabilityDatasetQualityStateMachine = ( + initialContext: ObservabilityDatasetQualityContext +) => + /** @xstate-layout N4IgpgJg5mDOIC5QHkBGswCcBuBDVAlgDYEAuAngDID2UAogB4AOR1mWAdAK4B2BfpArhIAvSAGIA2gAYAuolBNqsMgWo8FIBogDMOjgFYAbAEZpAdgCcRgwBYAHAcsGANCHK7bHWztsGTRjomevbmRpYAvhFuaBg4+MRkVLSMLGyc-KrCBCL8UABimNQAtgAqBMVg+cSkWADKWNgEAMZg4gCSAHLtpe0AgpTtAFp0ACIA+vkASsgAsuO9s3ST7ZSldFPjdRsAau0AwnQy8kggSiqC6praCAC0ZvrhRua24Tp2jiZuHggATEb2Dj-aQ2PQmcEGSFRGLoRoJEgUGj0ZisdiYDiZQTZXI8ApFYoAVUwRA63V6A2GY0mM3mBKmlGOmnOqiupxutx0Rg49ksv2kBl+9hMQp0oV+30QBnsXgsAX8lhevx0lki0RAsThhARyWRaTRHGa7Fwglx+3UpCKRCIWHE+2QnVKM0olA2432UzofXWo0Zp2Zlw0bMQt0FHCMAN85jC9k5tl+cYlCFCJg4QRMxnM0lsZkcRmh6th8S1SSRqVRGQEQlEkG4PAA1jxqAB3HillHpTB1UjGtqUZAAcXGdAAGgAFPsezZ1Upe5b7AASfU6-bGvsUyhZgdANyVBg4JmzRl+UtFFks9nsiYh0n3tn59lM-2FNnzGqLiURKXb+sxVZyNbwEgIDbPV6m7WpxD7QcR3HZBJy2Gd1jdRdl1XOQmQ3ANrklUxDFCSxDwVX4TAIq9BXMIFnnTc9gXseMojVRsIDgTQ3zwYtP11ctMAwi41C3LRg3TFMnheN4Pn8RN7h0CjpDk6QDyFcxiOkX5VRhOJ2I-HUyw7Wtf2xSBeM3bCEAot4LyFHl42cEEpNsSxDHkkwlTMKwH2cV9Cy07UQO4jFK2xPJChKcpKmqIhak7RoWjAYysKDO57Bvayswc0EDxeMiuVscwdEFAUBRci9fi8zT4RLL9QPRAzRGC-EiSIeL+NMjkuXeNT3PDF5fFcdxEDjFNlNBSxwQvXKdDKzVtL8vTDTAY08jNHgLWoK0sGa1lt2DTkOGsJUHNGuSSJ8RMXhTEw8t8S61LkpwpvfXyqv82r-wgTaBPZOjvGSz49F5RxzEvfqEDMdMwzvBwAnMAwoxIh6fMqri9NesQIFrBtm1bZ6Oy7HsPta3wfukP7lQKoGr2fDhpEsTllNCAwdAUya1TYirON0n9AurdHAIIYCcbRPHagJxKjBp-cDCzA9LDuqxLEph9qdp55yMZhSDAYiIgA */ + createMachine< + ObservabilityDatasetQualityContext, + ObservabilityDatasetQualityEvent, + ObservabilityDatasetQualityTypeState + >( + { + context: initialContext, + predictableActionArguments: true, + id: 'ObservabilityDatasetQuality', + initial: 'initializingFromUrl', + states: { + initializingFromUrl: { + invoke: { + src: 'initializeFromUrl', + }, + on: { + INITIALIZED_FROM_URL: { + target: 'creatingController', + actions: ['storeInitialUrlState'], + }, + }, + }, + creatingController: { + invoke: { + src: 'createController', + }, + on: { + CONTROLLER_CREATED: { + target: 'initialized', + actions: ['storeController', 'openDatasetFlyout'], + }, + }, + }, + initialized: { + invoke: { + src: 'subscribeToDatasetQualityState', + }, + states: { + unknownDatasetQualityState: { + on: { + DATASET_QUALITY_STATE_CHANGED: { + target: 'validDatasetQualityState', + actions: ['storeDatasetQualityState', 'updateUrlFromDatasetQualityState'], + }, + }, + }, + validDatasetQualityState: { + on: { + DATASET_QUALITY_STATE_CHANGED: { + actions: ['storeDatasetQualityState', 'updateUrlFromDatasetQualityState'], + target: 'validDatasetQualityState', + internal: true, + }, + }, + }, + }, + initial: 'unknownDatasetQualityState', + }, + }, + }, + { + actions: { + storeController: actions.assign((_context, event) => { + return 'controller' in event && event.type === 'CONTROLLER_CREATED' + ? { controller: event.controller } + : {}; + }), + storeInitialUrlState: actions.assign((context, event) => { + return 'stateFromUrl' in event && event.type === 'INITIALIZED_FROM_URL' + ? { + initialDatasetQualityState: { + ...('initialDatasetQualityState' in context + ? context.initialDatasetQualityState + : {}), + ...event.stateFromUrl, + }, + } + : {}; + }), + storeDatasetQualityState: actions.assign((_context, event) => { + return 'state' in event && event.type === 'DATASET_QUALITY_STATE_CHANGED' + ? { datasetQualityState: event.state } + : {}; + }), + }, + } + ); + +export interface ObservabilityDatasetQualityStateMachineDependencies { + createDatasetQualityController: CreateDatasetQualityController; + initialContext?: ObservabilityDatasetQualityContext; + toasts: IToasts; + urlStateStorageContainer: IKbnUrlStateStorage; +} + +export const createObservabilityDatasetQualityStateMachine = ({ + initialContext = DEFAULT_CONTEXT, + toasts, + urlStateStorageContainer, + createDatasetQualityController, +}: ObservabilityDatasetQualityStateMachineDependencies) => + createPureObservabilityDatasetQualityStateMachine(initialContext).withConfig({ + actions: { + updateUrlFromDatasetQualityState: updateUrlFromDatasetQualityState({ + urlStateStorageContainer, + }), + openDatasetFlyout, + }, + services: { + createController: createController({ createDatasetQualityController }), + initializeFromUrl: initializeFromUrl({ urlStateStorageContainer, toastsService: toasts }), + subscribeToDatasetQualityState, + }, + }); + +export type ObservabilityDatasetQualityService = InterpreterFrom< + typeof createObservabilityDatasetQualityStateMachine +>; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/types.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/types.ts new file mode 100644 index 0000000000000..4fa864b35951e --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/types.ts @@ -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 { + DatasetQualityController, + DatasetQualityPublicState, + DatasetQualityPublicStateUpdate, +} from '@kbn/dataset-quality-plugin/public/controller'; + +export type ObservabilityDatasetQualityContext = ObservabilityDatasetQualityTypeState['context']; + +export interface CommonObservabilityDatasetQualityContext { + initialDatasetQualityState: DatasetQualityPublicStateUpdate; +} + +export interface WithDatasetQualityState { + datasetQualityState: DatasetQualityPublicState; +} + +export interface WithController { + controller: DatasetQualityController; +} + +export type ObservabilityDatasetQualityEvent = + | { + type: 'INITIALIZED_FROM_URL'; + stateFromUrl?: DatasetQualityPublicStateUpdate; + } + | { + type: 'CONTROLLER_CREATED'; + controller: DatasetQualityController; + } + | { + type: 'DATASET_QUALITY_STATE_CHANGED'; + state: DatasetQualityPublicState; + }; + +export type ObservabilityDatasetQualityTypeState = + | { + value: 'initializingFromUrl' | 'creatingController'; + context: CommonObservabilityDatasetQualityContext; + } + | { + value: 'initialized' | { initialized: 'unknownDatasetQualityState' }; + context: CommonObservabilityDatasetQualityContext & WithController; + } + | { + value: { initialized: 'validDatasetQualityState' }; + context: CommonObservabilityDatasetQualityContext & WithDatasetQualityState & WithController; + }; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/url_schema_v1.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/url_schema_v1.ts new file mode 100644 index 0000000000000..07ec20798b5c2 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/url_schema_v1.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DatasetQualityPublicStateUpdate } from '@kbn/dataset-quality-plugin/public/controller'; +import * as rt from 'io-ts'; +import { datasetQualityUrlSchemaV1, deepCompactObject } from '../../../../common'; + +export const getStateFromUrlValue = ( + urlValue: datasetQualityUrlSchemaV1.UrlSchema +): DatasetQualityPublicStateUpdate => + deepCompactObject({ + table: urlValue.table, + flyout: urlValue.flyout, + }); + +export const getUrlValueFromState = ( + state: DatasetQualityPublicStateUpdate +): datasetQualityUrlSchemaV1.UrlSchema => + deepCompactObject({ + table: state.table, + flyout: state.flyout, + v: 1, + }); + +const stateFromUrlSchemaRT = new rt.Type< + DatasetQualityPublicStateUpdate, + datasetQualityUrlSchemaV1.UrlSchema, + datasetQualityUrlSchemaV1.UrlSchema +>( + 'stateFromUrlSchemaRT', + rt.never.is, + (urlSchema, _context) => rt.success(getStateFromUrlValue(urlSchema)), + getUrlValueFromState +); + +export const stateFromUntrustedUrlRT = + datasetQualityUrlSchemaV1.urlSchemaRT.pipe(stateFromUrlSchemaRT); diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/url_state_storage_service.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/url_state_storage_service.ts new file mode 100644 index 0000000000000..43cbd834729fc --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/dataset_quality/src/url_state_storage_service.ts @@ -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 { IToasts } from '@kbn/core-notifications-browser'; +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 * as rt from 'io-ts'; +import { InvokeCreator } from 'xstate'; +import { OBSERVABILITY_DATASET_QUALITY_URL_STATE_KEY } from '../../../../common'; +import type { ObservabilityDatasetQualityContext, ObservabilityDatasetQualityEvent } from './types'; +import * as urlSchemaV1 from './url_schema_v1'; + +interface ObservabilityDatasetQualityUrlStateDependencies { + toastsService: IToasts; + urlStateStorageContainer: IKbnUrlStateStorage; +} + +export const updateUrlFromDatasetQualityState = + ({ urlStateStorageContainer }: { urlStateStorageContainer: IKbnUrlStateStorage }) => + (context: ObservabilityDatasetQualityContext, event: ObservabilityDatasetQualityEvent) => { + if (!('datasetQualityState' in context)) { + return; + } + + const encodedUrlStateValues = urlSchemaV1.stateFromUntrustedUrlRT.encode( + context.datasetQualityState + ); + + urlStateStorageContainer.set( + OBSERVABILITY_DATASET_QUALITY_URL_STATE_KEY, + encodedUrlStateValues, + { + replace: true, + } + ); + }; + +export const initializeFromUrl = + ({ + toastsService, + urlStateStorageContainer, + }: ObservabilityDatasetQualityUrlStateDependencies): InvokeCreator< + ObservabilityDatasetQualityContext, + ObservabilityDatasetQualityEvent + > => + (_context, _event) => + (send) => { + const urlStateValues = + urlStateStorageContainer.get(OBSERVABILITY_DATASET_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)) + ); + send({ + type: 'INITIALIZED_FROM_URL', + stateFromUrl: undefined, + }); + } else { + send({ + type: 'INITIALIZED_FROM_URL', + stateFromUrl: stateValuesE.right, + }); + } + }; diff --git a/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/observability_log_explorer/src/url_schema_v1.ts b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/observability_log_explorer/src/url_schema_v1.ts index 916ea912c639a..895e84b0dd390 100644 --- a/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/observability_log_explorer/src/url_schema_v1.ts +++ b/x-pack/plugins/observability_solution/observability_logs_explorer/public/state_machines/observability_log_explorer/src/url_schema_v1.ts @@ -7,10 +7,10 @@ import { LogExplorerPublicStateUpdate } from '@kbn/logs-explorer-plugin/public'; import * as rt from 'io-ts'; -import { deepCompactObject, urlSchemaV1 } from '../../../../common'; +import { deepCompactObject, logExplorerUrlSchemaV1 } from '../../../../common'; export const getStateFromUrlValue = ( - urlValue: urlSchemaV1.UrlSchema + urlValue: logExplorerUrlSchemaV1.UrlSchema ): LogExplorerPublicStateUpdate => deepCompactObject({ chart: { @@ -31,8 +31,10 @@ export const getStateFromUrlValue = ( time: urlValue.time, }); -export const getUrlValueFromState = (state: LogExplorerPublicStateUpdate): urlSchemaV1.UrlSchema => - deepCompactObject({ +export const getUrlValueFromState = ( + state: LogExplorerPublicStateUpdate +): logExplorerUrlSchemaV1.UrlSchema => + deepCompactObject({ breakdownField: state.chart?.breakdownField, columns: state.grid?.columns, controls: state.controls, @@ -48,8 +50,8 @@ export const getUrlValueFromState = (state: LogExplorerPublicStateUpdate): urlSc const stateFromUrlSchemaRT = new rt.Type< LogExplorerPublicStateUpdate, - urlSchemaV1.UrlSchema, - urlSchemaV1.UrlSchema + logExplorerUrlSchemaV1.UrlSchema, + logExplorerUrlSchemaV1.UrlSchema >( 'stateFromUrlSchemaRT', rt.never.is, @@ -57,4 +59,5 @@ const stateFromUrlSchemaRT = new rt.Type< getUrlValueFromState ); -export const stateFromUntrustedUrlRT = urlSchemaV1.urlSchemaRT.pipe(stateFromUrlSchemaRT); +export const stateFromUntrustedUrlRT = + logExplorerUrlSchemaV1.urlSchemaRT.pipe(stateFromUrlSchemaRT); diff --git a/x-pack/test/dataset_quality_api_integration/tests/data_streams/data_stream_details.spec.ts b/x-pack/test/dataset_quality_api_integration/tests/data_streams/data_stream_details.spec.ts index 8c8643dc6ee04..2c809802e795e 100644 --- a/x-pack/test/dataset_quality_api_integration/tests/data_streams/data_stream_details.spec.ts +++ b/x-pack/test/dataset_quality_api_integration/tests/data_streams/data_stream_details.spec.ts @@ -56,21 +56,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns error when dataStream param is not provided', async () => { - expect( - (await expectToReject(() => callApiAs('datasetQualityLogsUser', encodeURIComponent(' ')))) - .message - ).to.contain('Data Stream name cannot be empty'); + const expectedMessage = 'Data Stream name cannot be empty'; + const err = await expectToReject(() => + callApiAs('datasetQualityLogsUser', encodeURIComponent(' ')) + ); + expect(err.res.status).to.be(400); + expect(err.res.body.message.indexOf(expectedMessage)).to.greaterThan(-1); }); - it('returns 404 if matching data stream is not available', async () => { + it('returns {} if matching data stream is not available', async () => { const nonExistentDataSet = 'Non-existent'; const nonExistentDataStream = `${type}-${nonExistentDataSet}-${namespace}`; - const expectedMessage = `"${nonExistentDataStream}" not found`; - const err = await expectToReject(() => - callApiAs('datasetQualityLogsUser', nonExistentDataStream) - ); - expect(err.res.status).to.be(404); - expect(err.res.body.message.indexOf(expectedMessage)).to.greaterThan(-1); + const resp = await callApiAs('datasetQualityLogsUser', nonExistentDataStream); + expect(resp.body).empty(); }); it('returns data stream details correctly', async () => { diff --git a/x-pack/test/functional/page_objects/observability_log_explorer.ts b/x-pack/test/functional/page_objects/observability_log_explorer.ts index 3f63b146756e6..b469238e5f9fb 100644 --- a/x-pack/test/functional/page_objects/observability_log_explorer.ts +++ b/x-pack/test/functional/page_objects/observability_log_explorer.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { OBSERVABILITY_LOGS_EXPLORER_URL_STATE_KEY, - urlSchemaV1, + logExplorerUrlSchemaV1, } from '@kbn/observability-logs-explorer-plugin/common'; import rison from '@kbn/rison'; import querystring from 'querystring'; @@ -105,7 +105,7 @@ const packages: IntegrationPackage[] = [ const initialPackages = packages.slice(0, 3); const additionalPackages = packages.slice(3); -const defaultPageState: urlSchemaV1.UrlSchema = { +const defaultPageState: logExplorerUrlSchemaV1.UrlSchema = { v: 1, time: { from: '2023-08-03T10:24:14.035Z', @@ -212,11 +212,11 @@ export function ObservabilityLogsExplorerPageObject({ async navigateTo({ pageState, }: { - pageState?: urlSchemaV1.UrlSchema; + pageState?: logExplorerUrlSchemaV1.UrlSchema; } = {}) { const queryStringParams = querystring.stringify({ [OBSERVABILITY_LOGS_EXPLORER_URL_STATE_KEY]: rison.encode( - urlSchemaV1.urlSchemaRT.encode({ + logExplorerUrlSchemaV1.urlSchemaRT.encode({ ...defaultPageState, ...pageState, })