[Dataset Quality]Migrate telemetry tests and remove flyout code (#190584)

## Summary

closes https://github.com/elastic/kibana/issues/184572

After the merge of the 1st
[PR](https://github.com/elastic/kibana/pull/189532) around Flyout
migration, this PR covers the remaining bits.

- [x] Adding same telemetry to the page which was present in the flyout
- [x] Create a Locator and use it in the Table to connect the main page
with Details page
- [x] Update locator in Unified Doc Viewer
- [x] Migrate all kind of tests which were present for flyout to the
page
- [x] Remove everything which was once called Dataset Quality Flyout.
- [x] Make build green - Yellow is also acceptable

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Achyut Jhunjhunwala 2024-08-27 11:12:53 +02:00 committed by GitHub
parent 32dd3730fa
commit 8431033910
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
83 changed files with 1178 additions and 4293 deletions

View file

@ -23,14 +23,6 @@ type TimeRangeConfig = {
refresh: RefreshInterval;
};
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type DatasetConfig = {
rawName: string;
type: string;
name: string;
namespace: string;
};
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type Filters = {
timeRange: TimeRangeConfig;
@ -38,7 +30,4 @@ type Filters = {
export interface DataQualityLocatorParams extends SerializableRecord {
filters?: Filters;
flyout?: {
dataset: DatasetConfig;
};
}

View file

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { SerializableRecord } from '@kbn/utility-types';
export const DATA_QUALITY_DETAILS_LOCATOR_ID = 'DATA_QUALITY_DETAILS_LOCATOR';
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type RefreshInterval = {
pause: boolean;
value: number;
};
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type TimeRangeConfig = {
from: string;
to: string;
refresh: RefreshInterval;
};
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type DegradedFieldsTable = {
page?: number;
rowsPerPage?: number;
sort?: {
field: string;
direction: 'asc' | 'desc';
};
};
export interface DataQualityDetailsLocatorParams extends SerializableRecord {
dataStream: string;
timeRange?: TimeRangeConfig;
breakdownField?: string;
degradedFields?: {
table?: DegradedFieldsTable;
};
}

View file

@ -7,6 +7,7 @@
*/
export * from './dataset_quality';
export * from './dataset_quality_details';
export * from './logs_explorer';
export * from './observability_logs_explorer';
export * from './observability_onboarding';

View file

@ -23,7 +23,10 @@ import {
import { i18n } from '@kbn/i18n';
import { orderBy } from 'lodash';
import { getRouterLinkProps } from '@kbn/router-utils';
import { DATA_QUALITY_LOCATOR_ID, DataQualityLocatorParams } from '@kbn/deeplinks-observability';
import {
DATA_QUALITY_DETAILS_LOCATOR_ID,
DataQualityDetailsLocatorParams,
} from '@kbn/deeplinks-observability';
import { BrowserUrlService } from '@kbn/share-plugin/public';
import { getUnifiedDocViewerServices } from '../../plugin';
@ -39,13 +42,6 @@ interface DegradedField {
values: string[];
}
interface ParamsForLocator {
dataStreamType: string;
dataStreamName: string;
dataStreamNamespace: string;
rawName: string;
}
interface TableOptions {
page: {
index: number;
@ -117,7 +113,7 @@ export const LogsOverviewDegradedFields = ({ rawDoc }: { rawDoc: DataTableRecord
const columns = getDegradedFieldsColumns();
const tableData = getDataFormattedForTable(ignoredFieldValues);
const paramsForLocator = getParamsForLocator(sourceFields);
const dataStream = getDataStreamRawName(sourceFields);
const accordionId = useGeneratedHtmlId({
prefix: qualityIssuesAccordionTitle,
@ -194,9 +190,7 @@ export const LogsOverviewDegradedFields = ({ rawDoc }: { rawDoc: DataTableRecord
buttonContent={accordionTitle}
paddingSize="m"
initialIsOpen={false}
extraAction={
<DatasetQualityLink urlService={urlService} paramsForLocator={paramsForLocator} />
}
extraAction={<DatasetQualityLink urlService={urlService} dataStream={dataStream} />}
data-test-subj="unifiedDocViewLogsOverviewDegradedFieldsAccordion"
>
<EuiBasicTable
@ -246,9 +240,9 @@ const getDataFormattedForTable = (
}));
};
const getParamsForLocator = (
const getDataStreamRawName = (
sourceFields: DataTableRecord['raw']['fields']
): ParamsForLocator | undefined => {
): string | undefined => {
if (sourceFields) {
const dataStreamTypeArr = sourceFields['data_stream.type'];
const dataStreamType = dataStreamTypeArr ? dataStreamTypeArr[0] : undefined;
@ -256,49 +250,35 @@ const getParamsForLocator = (
const dataStreamName = dataStreamNameArr ? dataStreamNameArr[0] : undefined;
const dataStreamNamespaceArr = sourceFields['data_stream.namespace'];
const dataStreamNamespace = dataStreamNamespaceArr ? dataStreamNamespaceArr[0] : undefined;
let rawName;
let dataStream;
if (dataStreamType && dataStreamName && dataStreamNamespace) {
rawName = `${dataStreamType}-${dataStreamName}-${dataStreamNamespace}`;
dataStream = `${dataStreamType}-${dataStreamName}-${dataStreamNamespace}`;
}
if (rawName) {
return {
dataStreamType,
dataStreamName,
dataStreamNamespace,
rawName,
};
}
return dataStream;
}
};
const DatasetQualityLink = React.memo(
({
urlService,
paramsForLocator,
dataStream,
}: {
urlService: BrowserUrlService;
paramsForLocator?: ParamsForLocator;
dataStream: string | undefined;
}) => {
const locator = urlService.locators.get<DataQualityLocatorParams>(DATA_QUALITY_LOCATOR_ID);
const locatorParams: DataQualityLocatorParams = paramsForLocator
? {
flyout: {
dataset: {
rawName: paramsForLocator.rawName,
type: paramsForLocator.dataStreamType,
name: paramsForLocator.dataStreamName,
namespace: paramsForLocator.dataStreamNamespace,
},
},
}
: {};
if (!dataStream) {
return null;
}
const locator = urlService.locators.get<DataQualityDetailsLocatorParams>(
DATA_QUALITY_DETAILS_LOCATOR_ID
);
const datasetQualityUrl = locator?.getRedirectUrl(locatorParams);
const datasetQualityUrl = locator?.getRedirectUrl({ dataStream });
const navigateToDatasetQuality = () => {
locator?.navigate(locatorParams);
locator?.navigate({ dataStream });
};
const datasetQualityLinkProps = getRouterLinkProps({
@ -306,7 +286,7 @@ const DatasetQualityLink = React.memo(
onClick: navigateToDatasetQuality,
});
return paramsForLocator ? (
return (
<EuiHeaderLink
{...datasetQualityLinkProps}
color="primary"
@ -316,6 +296,6 @@ const DatasetQualityLink = React.memo(
>
{datasetQualityLinkTitle}
</EuiHeaderLink>
) : null;
);
}
);

View file

@ -12,4 +12,8 @@ export const PLUGIN_NAME = i18n.translate('xpack.dataQuality.name', {
defaultMessage: 'Data Set Quality',
});
export { DATA_QUALITY_URL_STATE_KEY, datasetQualityUrlSchemaV1 } from './url_schema';
export {
DATA_QUALITY_URL_STATE_KEY,
datasetQualityUrlSchemaV1,
datasetQualityDetailsUrlSchemaV1,
} from './url_schema';

View file

@ -0,0 +1,50 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { setStateToKbnUrl } from '@kbn/kibana-utils-plugin/common';
import { ManagementAppLocatorParams } from '@kbn/management-plugin/common/locator';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { DataQualityDetailsLocatorParams } from '@kbn/deeplinks-observability';
import { datasetQualityDetailsUrlSchemaV1, DATA_QUALITY_URL_STATE_KEY } from '../url_schema';
import { deepCompactObject } from '../utils/deep_compact_object';
interface LocatorPathConstructionParams {
locatorParams: DataQualityDetailsLocatorParams;
useHash: boolean;
managementLocator: LocatorPublic<ManagementAppLocatorParams>;
}
export const constructDatasetQualityDetailsLocatorPath = async (
params: LocatorPathConstructionParams
) => {
const { locatorParams, useHash, managementLocator } = params;
const pageState = datasetQualityDetailsUrlSchemaV1.urlSchemaRT.encode(
deepCompactObject({
v: 1,
...locatorParams,
})
);
const managementPath = await managementLocator.getLocation({
sectionId: 'data',
appId: 'data_quality/details',
});
const path = setStateToKbnUrl(
DATA_QUALITY_URL_STATE_KEY,
pageState,
{ useHash, storeInHashQuery: false },
`${managementPath.app}${managementPath.path}`
);
return {
app: '',
path,
state: {},
};
};

View file

@ -20,7 +20,7 @@ interface LocatorPathConstructionParams {
export const constructDatasetQualityLocatorPath = async (params: LocatorPathConstructionParams) => {
const {
locatorParams: { filters, flyout },
locatorParams: { filters },
useHash,
managementLocator,
} = params;
@ -29,7 +29,6 @@ export const constructDatasetQualityLocatorPath = async (params: LocatorPathCons
deepCompactObject({
v: 1,
filters,
flyout,
})
);

View file

@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { LocatorDefinition } from '@kbn/share-plugin/public';
import {
DataQualityDetailsLocatorParams,
DATA_QUALITY_DETAILS_LOCATOR_ID,
} from '@kbn/deeplinks-observability';
import { DataQualityLocatorDependencies } from './types';
import { constructDatasetQualityDetailsLocatorPath } from './construct_dataset_quality_details_locator_path';
export class DatasetQualityDetailsLocatorDefinition
implements LocatorDefinition<DataQualityDetailsLocatorParams>
{
public readonly id = DATA_QUALITY_DETAILS_LOCATOR_ID;
constructor(protected readonly deps: DataQualityLocatorDependencies) {}
public readonly getLocation = async (params: DataQualityDetailsLocatorParams) => {
const { useHash, managementLocator } = this.deps;
return await constructDatasetQualityDetailsLocatorPath({
useHash,
managementLocator,
locatorParams: params,
});
};
}

View file

@ -5,13 +5,11 @@
* 2.0.
*/
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
import type { LocatorDefinition } from '@kbn/share-plugin/public';
import { DataQualityLocatorParams, DATA_QUALITY_LOCATOR_ID } from '@kbn/deeplinks-observability';
import { DataQualityLocatorDependencies } from './types';
import { constructDatasetQualityLocatorPath } from './construct_dataset_quality_locator_path';
export type DatasetQualityLocator = LocatorPublic<DataQualityLocatorParams>;
export class DatasetQualityLocatorDefinition
implements LocatorDefinition<DataQualityLocatorParams>
{

View file

@ -5,11 +5,6 @@
* 2.0.
*/
import { DatasetQualityLocator } from './dataset_quality_locator';
export * from './dataset_quality_locator';
export * from './dataset_quality_details_locator';
export * from './types';
export interface DataQualityLocators {
datasetQualityLocator: DatasetQualityLocator;
}

View file

@ -6,37 +6,7 @@
*/
import * as rt from 'io-ts';
import { degradedFieldRT, tableRT, timeRangeRT } from './common';
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,
}),
rt.exact(
rt.partial({
integration: integrationRT,
title: rt.string,
})
),
]);
export const flyoutRT = rt.exact(
rt.partial({
dataset: datasetRT,
insightsTimeRange: timeRangeRT,
breakdownField: rt.string,
degradedFields: degradedFieldRT,
})
);
import { tableRT, timeRangeRT } from './common';
export const filtersRT = rt.exact(
rt.partial({
@ -54,7 +24,6 @@ export const urlSchemaRT = rt.exact(
rt.partial({
v: rt.literal(1),
table: tableRT,
flyout: flyoutRT,
filters: filtersRT,
})
);

View file

@ -7,4 +7,4 @@
export { DATA_QUALITY_URL_STATE_KEY } from './common';
export * as datasetQualityUrlSchemaV1 from './dataset_quality_url_schema_v1';
export * as datasetQualityDetailsUrlSchemaV1 from './dataset_quality_detils_url_schema_v1';
export * as datasetQualityDetailsUrlSchemaV1 from './dataset_quality_details_url_schema_v1';

View file

@ -16,7 +16,10 @@ import {
AppPluginSetupDependencies,
} from './types';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
import { DatasetQualityLocatorDefinition } from '../common/locators';
import {
DatasetQualityLocatorDefinition,
DatasetQualityDetailsLocatorDefinition,
} from '../common/locators';
export class DataQualityPlugin
implements
@ -67,6 +70,12 @@ export class DataQualityPlugin
managementLocator,
})
);
share.url.locators.create(
new DatasetQualityDetailsLocatorDefinition({
useHash,
managementLocator,
})
);
}
return {};

View file

@ -44,13 +44,6 @@ export function DatasetQualityContextProvider({
});
datasetQualityController.service.start();
if (initialState?.flyout?.dataset) {
datasetQualityController.service.send({
type: 'OPEN_FLYOUT',
dataset: initialState.flyout.dataset,
});
}
setController(datasetQualityController);
const datasetQualityStateSubscription = datasetQualityController.state$.subscribe((state) => {

View file

@ -5,10 +5,7 @@
* 2.0.
*/
import {
DatasetQualityFlyoutOptions,
DatasetQualityPublicStateUpdate,
} from '@kbn/dataset-quality-plugin/public/controller/dataset_quality';
import { DatasetQualityPublicStateUpdate } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality';
import * as rt from 'io-ts';
import { deepCompactObject } from '../../../common/utils/deep_compact_object';
import { datasetQualityUrlSchemaV1 } from '../../../common/url_schema';
@ -18,7 +15,6 @@ export const getStateFromUrlValue = (
): DatasetQualityPublicStateUpdate =>
deepCompactObject<DatasetQualityPublicStateUpdate>({
table: urlValue.table,
flyout: getFlyoutFromUrlValue(urlValue.flyout),
filters: urlValue.filters,
});
@ -27,7 +23,6 @@ export const getUrlValueFromState = (
): datasetQualityUrlSchemaV1.UrlSchema =>
deepCompactObject<datasetQualityUrlSchemaV1.UrlSchema>({
table: state.table,
flyout: state.flyout,
filters: state.filters,
v: 1,
});
@ -45,25 +40,3 @@ const stateFromUrlSchemaRT = new rt.Type<
export const stateFromUntrustedUrlRT =
datasetQualityUrlSchemaV1.urlSchemaRT.pipe(stateFromUrlSchemaRT);
const getFlyoutFromUrlValue = (
flyout?: datasetQualityUrlSchemaV1.UrlSchema['flyout']
): DatasetQualityFlyoutOptions =>
deepCompactObject<DatasetQualityFlyoutOptions>({
...(flyout
? {
...flyout,
dataset: flyout.dataset
? {
...flyout.dataset,
integration: flyout.dataset.integration
? {
...flyout.dataset.integration,
datasets: {},
}
: undefined,
}
: undefined,
}
: {}),
});

View file

@ -17,6 +17,7 @@ export const getStateFromUrlValue = (
dataStream: urlValue.dataStream,
timeRange: urlValue.timeRange,
degradedFields: urlValue.degradedFields,
breakdownField: urlValue.breakdownField,
});
export const getUrlValueFromState = (
@ -26,6 +27,7 @@ export const getUrlValueFromState = (
dataStream: state.dataStream,
timeRange: state.timeRange,
degradedFields: state.degradedFields,
breakdownField: state.breakdownField,
v: 1,
});

View file

@ -14,7 +14,6 @@ import {
EuiIcon,
EuiLink,
EuiToolTip,
EuiButtonIcon,
EuiText,
formatNumber,
EuiSkeletonRectangle,
@ -25,27 +24,20 @@ import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import { css } from '@emotion/react';
import { BrowserUrlService } from '@kbn/share-plugin/public';
import {
DEGRADED_QUALITY_MINIMUM_PERCENTAGE,
POOR_QUALITY_MINIMUM_PERCENTAGE,
BYTE_NUMBER_FORMAT,
} from '../../../../common/constants';
import { DataStreamStat } from '../../../../common/data_streams_stats/data_stream_stat';
import { NavigationSource } from '../../../services/telemetry';
import { DatasetQualityIndicator, QualityIndicator } from '../../quality_indicator';
import { PrivilegesWarningIconWrapper, IntegrationIcon } from '../../common';
import { useRedirectLink } from '../../../hooks';
import { FlyoutDataset } from '../../../state_machines/dataset_quality_controller';
import { useDatasetRedirectLinkTelemetry, useRedirectLink } from '../../../hooks';
import { DegradedDocsPercentageLink } from './degraded_docs_percentage_link';
import { TimeRangeConfig } from '../../../../common/types';
import { DatasetQualityDetailsLink } from './dataset_quality_details_link';
const expandDatasetAriaLabel = i18n.translate('xpack.datasetQuality.expandLabel', {
defaultMessage: 'Expand',
});
const collapseDatasetAriaLabel = i18n.translate('xpack.datasetQuality.collapseLabel', {
defaultMessage: 'Collapse',
});
const nameColumnName = i18n.translate('xpack.datasetQuality.nameColumnName', {
defaultMessage: 'Data Set Name',
});
@ -162,52 +154,26 @@ export const getDatasetQualityTableColumns = ({
fieldFormats,
canUserMonitorDataset,
canUserMonitorAnyDataStream,
selectedDataset,
openFlyout,
loadingDataStreamStats,
loadingDegradedStats,
showFullDatasetNames,
isSizeStatsAvailable,
isActiveDataset,
timeRange,
urlService,
}: {
fieldFormats: FieldFormatsStart;
canUserMonitorDataset: boolean;
canUserMonitorAnyDataStream: boolean;
selectedDataset?: FlyoutDataset;
loadingDataStreamStats: boolean;
loadingDegradedStats: boolean;
showFullDatasetNames: boolean;
isSizeStatsAvailable: boolean;
openFlyout: (selectedDataset: FlyoutDataset) => void;
isActiveDataset: (lastActivity: number) => boolean;
timeRange: TimeRangeConfig;
urlService: BrowserUrlService;
}): Array<EuiBasicTableColumn<DataStreamStat>> => {
return [
{
name: '',
render: (dataStreamStat: DataStreamStat) => {
const isExpanded = dataStreamStat.rawName === selectedDataset?.rawName;
return (
<EuiButtonIcon
data-test-subj="datasetQualityExpandButton"
size="xs"
color="text"
onClick={() => openFlyout(dataStreamStat as FlyoutDataset)}
iconType={isExpanded ? 'minimize' : 'expand'}
title={!isExpanded ? expandDatasetAriaLabel : collapseDatasetAriaLabel}
aria-label={!isExpanded ? expandDatasetAriaLabel : collapseDatasetAriaLabel}
/>
);
},
width: '40px',
css: css`
&.euiTableCellContent {
padding: 0;
}
`,
},
{
name: (
<EuiTableHeader data-test-subj="datasetQualityNameColumn">{nameColumnName}</EuiTableHeader>
@ -215,20 +181,22 @@ export const getDatasetQualityTableColumns = ({
field: 'title',
sortable: true,
render: (title: string, dataStreamStat: DataStreamStat) => {
const { integration, name } = dataStreamStat;
const { integration, name, rawName } = dataStreamStat;
return (
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<IntegrationIcon integration={integration} />
</EuiFlexItem>
<EuiText size="s">{title}</EuiText>
{showFullDatasetNames && (
<EuiText size="xs" color="subdued">
<em>{name}</em>
</EuiText>
)}
</EuiFlexGroup>
<DatasetQualityDetailsLink urlService={urlService} dataStream={rawName}>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<IntegrationIcon integration={integration} />
</EuiFlexItem>
<EuiText size="s">{title}</EuiText>
{showFullDatasetNames && (
<EuiText size="xs" color="subdued">
<em>{name}</em>
</EuiText>
)}
</EuiFlexGroup>
</DatasetQualityDetailsLink>
);
},
},
@ -382,9 +350,10 @@ const RedirectLink = ({
title: string;
timeRange: TimeRangeConfig;
}) => {
const { sendTelemetry } = useDatasetRedirectLinkTelemetry({ rawName: dataStreamStat.rawName });
const redirectLinkProps = useRedirectLink({
dataStreamStat,
telemetry: { page: 'main', navigationSource: NavigationSource.Table },
sendTelemetry,
timeRangeConfig: timeRange,
});

View file

@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { BrowserUrlService } from '@kbn/share-plugin/public';
import {
DATA_QUALITY_DETAILS_LOCATOR_ID,
DataQualityDetailsLocatorParams,
} from '@kbn/deeplinks-observability';
import { getRouterLinkProps } from '@kbn/router-utils';
import { EuiHeaderLink } from '@elastic/eui';
export const DatasetQualityDetailsLink = React.memo(
({
urlService,
dataStream,
children,
}: {
urlService: BrowserUrlService;
dataStream: string;
children: React.ReactNode;
}) => {
const locator = urlService.locators.get<DataQualityDetailsLocatorParams>(
DATA_QUALITY_DETAILS_LOCATOR_ID
);
const datasetQualityUrl = locator?.getRedirectUrl({ dataStream });
const navigateToDatasetQuality = () => {
locator?.navigate({ dataStream });
};
const datasetQualityLinkDetailsProps = getRouterLinkProps({
href: datasetQualityUrl,
onClick: navigateToDatasetQuality,
});
return (
<EuiHeaderLink
{...datasetQualityLinkDetailsProps}
color="primary"
data-test-subj={`datasetQualityTableDetailsLink-${dataStream}`}
target="_blank"
size="xs"
>
{children}
</EuiHeaderLink>
);
}
);

View file

@ -8,8 +8,7 @@
import { EuiSkeletonRectangle, EuiFlexGroup, EuiLink } from '@elastic/eui';
import React from 'react';
import { _IGNORED } from '../../../../common/es_fields';
import { NavigationSource } from '../../../services/telemetry';
import { useRedirectLink } from '../../../hooks';
import { useDatasetRedirectLinkTelemetry, useRedirectLink } from '../../../hooks';
import { QualityPercentageIndicator } from '../../quality_indicator';
import { DataStreamStat } from '../../../../common/data_streams_stats/data_stream_stat';
import { TimeRangeConfig } from '../../../../common/types';
@ -27,13 +26,15 @@ export const DegradedDocsPercentageLink = ({
degradedDocs: { percentage, count },
} = dataStreamStat;
const { sendTelemetry } = useDatasetRedirectLinkTelemetry({
rawName: dataStreamStat.rawName,
query: { language: 'kuery', query: `${_IGNORED}: *` },
});
const redirectLinkProps = useRedirectLink({
dataStreamStat,
query: { language: 'kuery', query: `${_IGNORED}: *` },
telemetry: {
page: 'main',
navigationSource: NavigationSource.Table,
},
sendTelemetry,
timeRangeConfig: timeRange,
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import React from 'react';
import {
EuiBasicTable,
EuiEmptyPrompt,
@ -14,8 +15,6 @@ import {
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { dynamic } from '@kbn/shared-ux-utility';
import React from 'react';
import {
fullDatasetNameDescription,
fullDatasetNameLabel,
@ -27,8 +26,6 @@ import {
import { useDatasetQualityTable } from '../../../hooks';
import { DescriptiveSwitch } from '../../common/descriptive_switch';
const Flyout = dynamic(() => import('../../flyout/flyout'));
export const Table = () => {
const {
sort,
@ -38,8 +35,6 @@ export const Table = () => {
columns,
loading,
resultsCount,
selectedDataset,
closeFlyout,
showInactiveDatasets,
showFullDatasetNames,
canUserMonitorDataset,
@ -107,7 +102,6 @@ export const Table = () => {
)
}
/>
{selectedDataset && <Flyout dataset={selectedDataset} closeFlyout={closeFlyout} />}
</>
);
};

View file

@ -4,9 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import React, { useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui';
import { useDatasetQualityDetailsState } from '../../hooks';
import { useDatasetDetailsTelemetry, useDatasetQualityDetailsState } from '../../hooks';
import { DataStreamNotFoundPrompt } from './index_not_found_prompt';
import { Header } from './header';
import { Overview } from './overview';
@ -16,10 +16,15 @@ import { Details } from './details';
// eslint-disable-next-line import/no-default-export
export default function DatasetQualityDetails() {
const { isIndexNotFoundError, dataStream } = useDatasetQualityDetailsState();
const { startTracking } = useDatasetDetailsTelemetry();
useEffect(() => {
startTracking();
}, [startTracking]);
return isIndexNotFoundError ? (
<DataStreamNotFoundPrompt dataStream={dataStream} />
) : (
<EuiFlexGroup direction="column" gutterSize="l">
<EuiFlexGroup direction="column" gutterSize="l" data-test-subj="datasetDetailsContainer">
<EuiFlexItem grow={false}>
<Header />
<EuiHorizontalRule />

View file

@ -32,7 +32,12 @@ export function FieldsList({
<EuiFlexGroup direction="column" gutterSize="none">
{fields.map(({ fieldTitle, fieldValue, isLoading: isFieldLoading, actionsMenu }, index) => (
<Fragment key={index + fieldTitle}>
<EuiFlexGroup>
<EuiFlexGroup
data-test-subj={`datasetQualityDetailsFieldsList-${fieldTitle
.toLowerCase()
.split(' ')
.join('_')}`}
>
<EuiFlexItem grow={1}>
<EuiTitle size="xxs">
<span>{fieldTitle}</span>

View file

@ -18,19 +18,30 @@ import {
import { css } from '@emotion/react';
import React from 'react';
import { openInDiscoverText, openInLogsExplorerText } from '../../../common/translations';
import { useDatasetQualityDetailsRedirectLink, useDatasetQualityDetailsState } from '../../hooks';
import {
useDatasetDetailsRedirectLinkTelemetry,
useDatasetDetailsTelemetry,
useDatasetQualityDetailsState,
useRedirectLink,
} from '../../hooks';
import { IntegrationIcon } from '../common';
export function Header() {
const { datasetDetails, timeRange, integrationDetails, loadingState } =
useDatasetQualityDetailsState();
const { navigationSources } = useDatasetDetailsTelemetry();
const { rawName, name: title } = datasetDetails;
const euiShadow = useEuiShadow('s');
const { euiTheme } = useEuiTheme();
const redirectLinkProps = useDatasetQualityDetailsRedirectLink({
const { sendTelemetry } = useDatasetDetailsRedirectLinkTelemetry({
navigationSource: navigationSources.Header,
});
const redirectLinkProps = useRedirectLink({
dataStreamStat: datasetDetails,
timeRangeConfig: timeRange,
sendTelemetry,
});
const pageTitle = integrationDetails?.integration?.datasets?.[datasetDetails.name] ?? title;
@ -38,15 +49,15 @@ export function Header() {
return !loadingState.integrationDetailsLoaded ? (
<EuiSkeletonTitle
size="s"
data-test-subj="datasetQualityFlyoutIntegrationLoading"
className="datasetQualityFlyoutIntegrationLoading"
data-test-subj="datasetQualityDetailsIntegrationLoading"
className="datasetQualityDetailsIntegrationLoading"
/>
) : (
<EuiFlexGroup justifyContent="flexStart">
<EuiFlexItem grow>
<EuiFlexGroup gutterSize="m" alignItems="flexStart" direction="column">
<EuiFlexGroup gutterSize="m" justifyContent="flexStart" alignItems="center">
<EuiTitle data-test-subj="datasetQualityFlyoutTitle" size="l">
<EuiTitle data-test-subj="datasetQualityDetailsTitle" size="l">
<h2>{pageTitle}</h2>
</EuiTitle>
<div
@ -74,7 +85,7 @@ export function Header() {
alignItems="center"
>
<EuiButton
data-test-subj="datasetQualityHeaderButton"
data-test-subj="datasetQualityDetailsHeaderButton"
size="s"
{...redirectLinkProps.linkProps}
iconType={

View file

@ -6,7 +6,7 @@
*/
import React from 'react';
import { EuiEmptyPrompt } from '@elastic/eui';
import { EuiEmptyPrompt, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
const emptyPromptTitle = i18n.translate('xpack.datasetQuality.details.emptypromptTitle', {
@ -23,7 +23,19 @@ const emptyPromptBody = (dataStream: string) =>
export function DataStreamNotFoundPrompt({ dataStream }: { dataStream: string }) {
const promptTitle = <h2>{emptyPromptTitle}</h2>;
const promptBody = <p>{emptyPromptBody(dataStream)}</p>;
const promptBody = (
<EuiText data-test-subj="datasetQualityDetailsEmptyPromptBody">
<p>{emptyPromptBody(dataStream)}</p>
</EuiText>
);
return <EuiEmptyPrompt iconType="error" color="danger" title={promptTitle} body={promptBody} />;
return (
<EuiEmptyPrompt
iconType="error"
color="danger"
title={promptTitle}
body={promptBody}
data-test-subj="datasetQualityDetailsEmptyPrompt"
/>
);
}

View file

@ -14,7 +14,7 @@ import { KibanaErrorBoundary } from '@kbn/shared-ux-error-boundary';
import { flyoutDegradedDocsTrendText } from '../../../../../../common/translations';
import { useKibanaContextForPlugin } from '../../../../../utils';
import { TimeRangeConfig } from '../../../../../../common/types';
import { useDegradedDocs } from '../../../../../hooks/use_degraded_docs';
import { useDegradedDocsChart } from '../../../../../hooks';
const CHART_HEIGHT = 180;
const DISABLED_ACTIONS = [
@ -26,7 +26,7 @@ const DISABLED_ACTIONS = [
interface DegradedDocsChartProps
extends Pick<
ReturnType<typeof useDegradedDocs>,
ReturnType<typeof useDegradedDocsChart>,
'attributes' | 'isChartLoading' | 'onChartLoading' | 'extraActions'
> {
timeRange: TimeRangeConfig;

View file

@ -29,13 +29,15 @@ import {
openInLogsExplorerText,
overviewDegradedDocsText,
} from '../../../../../../common/translations';
import { useDegradedDocs } from '../../../../../hooks/use_degraded_docs';
import { DegradedDocsChart } from './degraded_docs_chart';
import {
useDatasetQualityDetailsRedirectLink,
useDatasetDetailsRedirectLinkTelemetry,
useDatasetQualityDetailsState,
useDegradedDocsChart,
useRedirectLink,
} from '../../../../../hooks';
import { _IGNORED } from '../../../../../../common/es_fields';
import { NavigationSource } from '../../../../../services/telemetry';
const degradedDocsTooltip = (
<FormattedMessage
@ -55,7 +57,7 @@ const degradedDocsTooltip = (
// eslint-disable-next-line import/no-default-export
export default function DegradedDocs({ lastReloadTime }: { lastReloadTime: number }) {
const { timeRange, updateTimeRange, datasetDetails } = useDatasetQualityDetailsState();
const { dataView, breakdown, ...chartProps } = useDegradedDocs();
const { dataView, breakdown, ...chartProps } = useDegradedDocsChart();
const accordionId = useGeneratedHtmlId({
prefix: overviewDegradedDocsText,
@ -65,10 +67,16 @@ export default function DegradedDocs({ lastReloadTime }: { lastReloadTime: numbe
undefined
);
const degradedDocLinkLogsExplorer = useDatasetQualityDetailsRedirectLink({
const { sendTelemetry } = useDatasetDetailsRedirectLinkTelemetry({
query: { language: 'kuery', query: `${_IGNORED}: *` },
navigationSource: NavigationSource.Trend,
});
const degradedDocLinkLogsExplorer = useRedirectLink({
dataStreamStat: datasetDetails,
timeRangeConfig: timeRange,
query: { language: 'kuery', query: `${_IGNORED}: *` },
sendTelemetry,
});
useEffect(() => {

View file

@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSkeletonTitle, EuiText } from '
import { css } from '@emotion/react';
import { euiThemeVars } from '@kbn/ui-theme';
import { PrivilegesWarningIconWrapper } from '../../../common';
import { notAvailableLabel } from '../../../../../common/translations';
const verticalRule = css`
width: 1px;
@ -95,8 +96,8 @@ export function PanelIndicator({
<></>
</PrivilegesWarningIconWrapper>
{userHasPrivilege && (
<EuiText size="m">
<h3>{value}</h3>
<EuiText size="m" data-test-subj={`datasetQualityDetailsSummaryKpiValue-${label}`}>
<h3>{userHasPrivilege ? value : notAvailableLabel}</h3>
</EuiText>
)}
</>

View file

@ -1,65 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import { DataStreamDetails, DataStreamSettings } from '../../../common/data_streams_stats';
import {
datasetCreatedOnText,
flyoutDatasetDetailsText,
datasetLastActivityText,
} from '../../../common/translations';
import { FieldsList, FieldsListLoading } from './fields_list';
interface DatasetSummaryProps {
fieldFormats: FieldFormatsStart;
dataStreamSettings?: DataStreamSettings;
dataStreamSettingsLoading: boolean;
dataStreamDetails?: DataStreamDetails;
dataStreamDetailsLoading: boolean;
}
export function DatasetSummary({
dataStreamSettings,
dataStreamSettingsLoading,
dataStreamDetails,
dataStreamDetailsLoading,
fieldFormats,
}: DatasetSummaryProps) {
const dataFormatter = fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.DATE, [
ES_FIELD_TYPES.DATE,
]);
const formattedLastActivity = dataStreamDetails?.lastActivity
? dataFormatter.convert(dataStreamDetails?.lastActivity)
: '-';
const formattedCreatedOn = dataStreamSettings?.createdOn
? dataFormatter.convert(dataStreamSettings.createdOn)
: '-';
return (
<FieldsList
title={flyoutDatasetDetailsText}
fields={[
{
fieldTitle: datasetLastActivityText,
fieldValue: formattedLastActivity,
isLoading: dataStreamDetailsLoading,
},
{
fieldTitle: datasetCreatedOnText,
fieldValue: formattedCreatedOn,
isLoading: dataStreamSettingsLoading,
},
]}
/>
);
}
export function DatasetSummaryLoading() {
return <FieldsListLoading />;
}

View file

@ -1,113 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useEffect, useState } from 'react';
import { css } from '@emotion/react';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiSpacer,
EuiTitle,
EuiToolTip,
EuiIcon,
EuiCode,
OnTimeChangeProps,
EuiSkeletonRectangle,
} from '@elastic/eui';
import { UnifiedBreakdownFieldSelector } from '@kbn/unified-histogram-plugin/public';
import type { DataViewField } from '@kbn/data-views-plugin/common';
import { useDegradedDocsChart } from '../../../hooks';
import { DEFAULT_TIME_RANGE, DEFAULT_DATEPICKER_REFRESH } from '../../../../common/constants';
import { overviewDegradedDocsText } from '../../../../common/translations';
import { DegradedDocsChart } from '../../dataset_quality_details/overview/document_trends/degraded_docs/degraded_docs_chart';
import { TimeRangeConfig } from '../../../../common/types';
export function DegradedDocs({
dataStream,
timeRange = { ...DEFAULT_TIME_RANGE, refresh: DEFAULT_DATEPICKER_REFRESH },
lastReloadTime,
onTimeRangeChange,
}: {
dataStream?: string;
timeRange?: TimeRangeConfig;
lastReloadTime: number;
onTimeRangeChange: (props: Pick<OnTimeChangeProps, 'start' | 'end'>) => void;
}) {
const { dataView, breakdown, ...chartProps } = useDegradedDocsChart({ dataStream });
const [breakdownDataViewField, setBreakdownDataViewField] = useState<DataViewField | undefined>(
undefined
);
useEffect(() => {
if (breakdown.dataViewField && breakdown.fieldSupportsBreakdown) {
setBreakdownDataViewField(breakdown.dataViewField);
} else {
setBreakdownDataViewField(undefined);
}
if (breakdown.dataViewField && !breakdown.fieldSupportsBreakdown) {
// TODO: If needed, notify user that the field is not breakable
}
}, [setBreakdownDataViewField, breakdown]);
return (
<EuiPanel hasBorder grow={false}>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem
css={css`
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
gap: 4px;
`}
>
<EuiTitle size="xxxs">
<h6>{overviewDegradedDocsText}</h6>
</EuiTitle>
<EuiToolTip content={degradedDocsTooltip}>
<EuiIcon size="m" color="subdued" type="questionInCircle" className="eui-alignTop" />
</EuiToolTip>
</EuiFlexItem>
<EuiSkeletonRectangle width={160} height={32} isLoading={!dataView}>
<UnifiedBreakdownFieldSelector
dataView={dataView!}
breakdown={{ field: breakdownDataViewField }}
onBreakdownFieldChange={breakdown.onChange}
/>
</EuiSkeletonRectangle>
</EuiFlexGroup>
<EuiSpacer size="m" />
<DegradedDocsChart
{...chartProps}
timeRange={timeRange}
lastReloadTime={lastReloadTime}
onTimeRangeChange={onTimeRangeChange}
/>
</EuiPanel>
);
}
const degradedDocsTooltip = (
<FormattedMessage
id="xpack.datasetQuality.flyoutDegradedDocsTooltip"
defaultMessage="The percentage of degraded documents —documents with the {ignoredProperty} property— in your data set."
values={{
ignoredProperty: (
<EuiCode language="json" transparentBackground>
_ignored
</EuiCode>
),
}}
/>
);

View file

@ -1,61 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiBasicTableColumn } from '@elastic/eui';
import { FieldFormat } from '@kbn/field-formats-plugin/common';
import { formatNumber } from '@elastic/eui';
import { DegradedField } from '../../../../common/api_types';
import { SparkPlot } from './spark_plot';
import { NUMBER_FORMAT } from '../../../../common/constants';
const fieldColumnName = i18n.translate('xpack.datasetQuality.flyout.degradedField.field', {
defaultMessage: 'Field',
});
const countColumnName = i18n.translate('xpack.datasetQuality.flyout.degradedField.count', {
defaultMessage: 'Docs count',
});
const lastOccurrenceColumnName = i18n.translate(
'xpack.datasetQuality.flyout.degradedField.lastOccurrence',
{
defaultMessage: 'Last occurrence',
}
);
export const getDegradedFieldsColumns = ({
dateFormatter,
isLoading,
}: {
dateFormatter: FieldFormat;
isLoading: boolean;
}): Array<EuiBasicTableColumn<DegradedField>> => [
{
name: fieldColumnName,
field: 'name',
},
{
name: countColumnName,
sortable: true,
field: 'count',
render: (_, { count, timeSeries }) => {
const countValue = formatNumber(count, NUMBER_FORMAT);
return <SparkPlot series={timeSeries} valueLabel={countValue} isLoading={isLoading} />;
},
},
{
name: lastOccurrenceColumnName,
sortable: true,
field: 'lastOccurrence',
render: (lastOccurrence: number) => {
return dateFormatter.convert(lastOccurrence);
},
},
];

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { EuiFlexGroup, EuiPanel, EuiTitle, EuiIconTip } from '@elastic/eui';
import { flyoutImprovementText, flyoutImprovementTooltip } from '../../../../common/translations';
import { DegradedFieldTable } from './table';
export function DegradedFields() {
return (
<EuiPanel hasBorder grow={false}>
<EuiFlexGroup responsive={false} alignItems="center" gutterSize="xs">
<EuiTitle size="xxxs">
<h6>{flyoutImprovementText}</h6>
</EuiTitle>
<EuiIconTip content={flyoutImprovementTooltip} color="subdued" size="m" />
</EuiFlexGroup>
<DegradedFieldTable />
</EuiPanel>
);
}

View file

@ -1,101 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiIcon,
EuiLoadingChart,
euiPaletteColorBlind,
useEuiTheme,
} from '@elastic/eui';
import React from 'react';
import { ScaleType, Settings, Tooltip, Chart, BarSeries } from '@elastic/charts';
import { i18n } from '@kbn/i18n';
import { Coordinate } from '../../../../common/types';
export function SparkPlot({
valueLabel,
isLoading,
series,
}: {
valueLabel: React.ReactNode;
isLoading: boolean;
series?: Coordinate[] | null;
}) {
return (
<EuiFlexGroup justifyContent="flexStart" gutterSize="s" responsive={false} alignItems="center">
<EuiFlexItem grow={false}>
<SparkPlotItem isLoading={isLoading} series={series} />
</EuiFlexItem>
<EuiFlexItem grow={false}>{valueLabel}</EuiFlexItem>
</EuiFlexGroup>
);
}
function SparkPlotItem({
isLoading,
series,
}: {
isLoading: boolean;
series?: Coordinate[] | null;
}) {
const { euiTheme } = useEuiTheme();
const chartSize = {
height: euiTheme.size.l,
width: '80px',
};
const commonStyle = {
...chartSize,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
};
const palette = euiPaletteColorBlind({ rotations: 2 });
if (isLoading) {
return (
<div style={commonStyle}>
<EuiLoadingChart mono />
</div>
);
}
if (hasValidTimeSeries(series)) {
return (
<div
style={{ backgroundColor: `${palette[0]}`, padding: 1, height: '100%' }}
data-test-subj="datasetQualitySparkPlot"
>
<Chart size={chartSize}>
<Settings showLegend={false} locale={i18n.getLocale()} />
<Tooltip type="none" />
<BarSeries
id="barseries"
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
data={series}
color={palette[1]}
/>
</Chart>
</div>
);
}
return (
<div style={commonStyle}>
<EuiIcon type="visLine" color={euiTheme.colors.mediumShade} />
</div>
);
}
function hasValidTimeSeries(series?: Coordinate[] | null): series is Coordinate[] {
return !!series?.some((point) => point.y !== 0);
}

View file

@ -1,54 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiBasicTable, EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import { useDatasetQualityDegradedField } from '../../../hooks';
import { getDegradedFieldsColumns } from './columns';
import {
overviewDegradedFieldsTableLoadingText,
overviewDegradedFieldsTableNoData,
} from '../../../../common/translations';
export const DegradedFieldTable = () => {
const { isLoading, pagination, renderedItems, onTableChange, sort, fieldFormats } =
useDatasetQualityDegradedField();
const dateFormatter = fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.DATE, [
ES_FIELD_TYPES.DATE,
]);
const columns = getDegradedFieldsColumns({ dateFormatter, isLoading });
return (
<EuiBasicTable
tableLayout="fixed"
columns={columns}
items={renderedItems ?? []}
loading={isLoading}
sorting={sort}
onChange={onTableChange}
pagination={pagination}
data-test-subj="datasetQualityFlyoutDegradedFieldTable"
rowProps={{
'data-test-subj': 'datasetQualityFlyoutDegradedTableRow',
}}
noItemsMessage={
isLoading ? (
overviewDegradedFieldsTableLoadingText
) : (
<EuiEmptyPrompt
data-test-subj="datasetQualityFlyoutDegradedTableNoData"
layout="vertical"
title={<h2>{overviewDegradedFieldsTableNoData}</h2>}
hasBorder={false}
titleSize="m"
/>
)
}
/>
);
};

View file

@ -1,94 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { ReactNode, Fragment } from 'react';
import {
EuiFlexGroup,
EuiPanel,
EuiFlexItem,
EuiSpacer,
EuiTitle,
EuiHorizontalRule,
EuiSkeletonTitle,
EuiSkeletonText,
EuiSkeletonRectangle,
} from '@elastic/eui';
export function FieldsList({
title,
fields,
actionsMenu: ActionsMenu,
dataTestSubj = `datasetQualityFlyoutFieldsList-${title.toLowerCase().split(' ').join('_')}`,
}: {
title: string;
fields: Array<{ fieldTitle: string; fieldValue: ReactNode; isLoading: boolean }>;
actionsMenu?: ReactNode;
dataTestSubj?: string;
}) {
return (
<EuiPanel hasBorder grow={false} data-test-subj={dataTestSubj}>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiTitle size="s">
<span>{title}</span>
</EuiTitle>
<EuiFlexItem grow={false}>{ActionsMenu}</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer />
<EuiFlexGroup direction="column" gutterSize="none">
{fields.map(({ fieldTitle, fieldValue, isLoading: isFieldLoading }, index) => (
<Fragment key={index + fieldTitle}>
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiTitle size="xxs">
<span>{fieldTitle}</span>
</EuiTitle>
</EuiFlexItem>
<EuiSkeletonRectangle width={260} isLoading={isFieldLoading} title={title}>
<EuiFlexItem grow={4} data-test-subj="datasetQualityFlyoutFieldValue">
{fieldValue}
</EuiFlexItem>
</EuiSkeletonRectangle>
</EuiFlexGroup>
{index < fields.length - 1 ? <EuiHorizontalRule margin="s" /> : null}
</Fragment>
))}
</EuiFlexGroup>
</EuiPanel>
);
}
export function FieldsListLoading() {
return (
<EuiPanel hasBorder grow={false}>
<EuiFlexGroup direction="column" gutterSize="none">
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiSkeletonTitle size="s" />
</EuiFlexItem>
</EuiFlexGroup>
<EuiHorizontalRule margin="s" />
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiSkeletonText size="m" lines={1} />
</EuiFlexItem>
<EuiFlexItem grow={2}>
<EuiSkeletonText lines={1} />
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiSkeletonText size="m" lines={1} />
</EuiFlexItem>
<EuiFlexItem grow={2}>
<EuiSkeletonText lines={1} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -1,147 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { Fragment, useEffect } from 'react';
import { css } from '@emotion/react';
import {
EuiButtonEmpty,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
EuiFlyoutBody,
EuiFlyoutFooter,
EuiSpacer,
EuiHorizontalRule,
EuiPanel,
EuiSkeletonRectangle,
} from '@elastic/eui';
import { dynamic } from '@kbn/shared-ux-utility';
import { flyoutCancelText } from '../../../common/translations';
import { useDatasetQualityFlyout, useDatasetDetailsTelemetry } from '../../hooks';
import { DatasetSummary, DatasetSummaryLoading } from './dataset_summary';
import { Header } from './header';
import { FlyoutProps } from './types';
import { BasicDataStream } from '../../../common/types';
const FlyoutSummary = dynamic(() => import('./flyout_summary/flyout_summary'));
const IntegrationSummary = dynamic(() => import('./integration_summary'));
// Allow for lazy loading
// eslint-disable-next-line import/no-default-export
export default function Flyout({ dataset, closeFlyout }: FlyoutProps) {
const {
dataStreamStat,
dataStreamSettings,
dataStreamDetails,
isNonAggregatable,
fieldFormats,
timeRange,
loadingState,
flyoutLoading,
integration,
} = useDatasetQualityFlyout();
const linkDetails: BasicDataStream = {
name: dataset.name,
rawName: dataset.rawName,
integration: integration?.integrationDetails,
type: dataset.type,
namespace: dataset.namespace,
};
const title = integration?.integrationDetails?.datasets?.[dataset.name] ?? dataset.name;
const { startTracking } = useDatasetDetailsTelemetry();
useEffect(() => {
startTracking();
}, [startTracking]);
return (
<EuiFlyout
onClose={closeFlyout}
ownFocus={true}
data-component-name={'datasetQualityFlyout'}
data-test-subj="datasetQualityFlyout"
>
{flyoutLoading ? (
<EuiSkeletonRectangle width="100%" height={80} />
) : (
<>
<Header
linkDetails={linkDetails}
loading={!loadingState.datasetIntegrationDone}
title={title}
timeRange={timeRange}
/>
<EuiFlyoutBody css={flyoutBodyStyles} data-test-subj="datasetQualityFlyoutBody">
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="l">
<FlyoutSummary
dataStream={dataset.rawName}
dataStreamStat={dataStreamStat}
dataStreamDetails={dataStreamDetails}
dataStreamDetailsLoading={loadingState.dataStreamDetailsLoading}
timeRange={timeRange}
isNonAggregatable={isNonAggregatable}
/>
</EuiPanel>
<EuiHorizontalRule margin="none" />
<EuiPanel hasBorder={false} hasShadow={false} paddingSize="l">
{loadingState.dataStreamDetailsLoading && loadingState.dataStreamSettingsLoading ? (
<DatasetSummaryLoading />
) : dataStreamStat ? (
<Fragment>
<DatasetSummary
dataStreamSettings={dataStreamSettings}
dataStreamSettingsLoading={loadingState.dataStreamSettingsLoading}
dataStreamDetails={dataStreamDetails}
dataStreamDetailsLoading={loadingState.dataStreamDetailsLoading}
fieldFormats={fieldFormats}
/>
{integration?.integrationDetails && (
<>
<EuiSpacer />
<IntegrationSummary
integration={integration.integrationDetails}
dashboards={integration?.dashboards ?? []}
dashboardsLoading={loadingState.datasetIntegrationDashboardLoading}
/>
</>
)}
</Fragment>
) : null}
</EuiPanel>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<EuiButtonEmpty
data-test-subj="datasetQualityFlyoutButton"
iconType="cross"
onClick={closeFlyout}
flush="left"
>
{flyoutCancelText}
</EuiButtonEmpty>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlyoutFooter>
</>
)}
</EuiFlyout>
);
}
const flyoutBodyStyles = css`
.euiFlyoutBody__overflowContent {
padding: 0;
}
`;

View file

@ -1,186 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import {
OnRefreshProps,
OnTimeChangeProps,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiCallOut,
EuiLink,
EuiCode,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { DegradedDocs } from '../degraded_docs_trend/degraded_docs';
import { DataStreamDetails } from '../../../../common/api_types';
import { DEFAULT_TIME_RANGE, DEFAULT_DATEPICKER_REFRESH } from '../../../../common/constants';
import { useDatasetQualityContext } from '../../dataset_quality/context';
import { FlyoutSummaryHeader } from './flyout_summary_header';
import { FlyoutSummaryKpis, FlyoutSummaryKpisLoading } from './flyout_summary_kpis';
import { DegradedFields } from '../degraded_fields/degraded_fields';
import { TimeRangeConfig } from '../../../../common/types';
import { FlyoutDataset } from '../../../state_machines/dataset_quality_controller';
const nonAggregatableWarningTitle = i18n.translate('xpack.datasetQuality.nonAggregatable.title', {
defaultMessage: 'Your request may take longer to complete',
});
const nonAggregatableWarningDescription = (dataset: string) => (
<FormattedMessage
id="xpack.datasetQuality.flyout.nonAggregatable.description"
defaultMessage="{description}"
values={{
description: (
<FormattedMessage
id="xpack.datasetQuality.flyout.nonAggregatable.warning"
defaultMessage="{dataset}does not support _ignored aggregation and may cause delays when querying data. {howToFixIt}"
values={{
dataset: (
<EuiCode language="json" transparentBackground>
{dataset}
</EuiCode>
),
howToFixIt: (
<FormattedMessage
id="xpack.datasetQuality.flyout.nonAggregatable.howToFixIt"
defaultMessage="Manually {rolloverLink} this data set to prevent future delays."
values={{
rolloverLink: (
<EuiLink
external
target="_blank"
data-test-subj="datasetQualityFlyoutNonAggregatableHowToFixItLink"
href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-rollover-index.html"
>
{i18n.translate(
'xpack.datasetQuality.flyout.nonAggregatableDatasets.link.title',
{
defaultMessage: 'rollover',
}
)}
</EuiLink>
),
}}
/>
),
}}
/>
),
}}
/>
);
// Allow for lazy loading
// eslint-disable-next-line import/no-default-export
export default function FlyoutSummary({
dataStream,
dataStreamStat,
dataStreamDetails,
isNonAggregatable,
dataStreamDetailsLoading,
timeRange = { ...DEFAULT_TIME_RANGE, refresh: DEFAULT_DATEPICKER_REFRESH },
}: {
dataStream: string;
dataStreamStat?: FlyoutDataset;
dataStreamDetails?: DataStreamDetails;
dataStreamDetailsLoading: boolean;
timeRange?: TimeRangeConfig;
isNonAggregatable?: boolean;
}) {
const { service } = useDatasetQualityContext();
const [lastReloadTime, setLastReloadTime] = useState<number>(Date.now());
const updateTimeRange = useCallback(
({ start, end, refreshInterval }: OnRefreshProps) => {
service.send({
type: 'UPDATE_INSIGHTS_TIME_RANGE',
timeRange: {
from: start,
to: end,
refresh: { ...DEFAULT_DATEPICKER_REFRESH, value: refreshInterval },
},
});
},
[service]
);
const handleTimeChange = useCallback(
({ isInvalid, ...timeRangeProps }: OnTimeChangeProps) => {
if (!isInvalid) {
updateTimeRange({ refreshInterval: timeRange.refresh.value, ...timeRangeProps });
}
},
[updateTimeRange, timeRange.refresh]
);
const handleTimeRangeChange = useCallback(
({ start, end }: Pick<OnTimeChangeProps, 'start' | 'end'>) => {
updateTimeRange({ start, end, refreshInterval: timeRange.refresh.value });
},
[updateTimeRange, timeRange.refresh]
);
const handleRefresh = useCallback(
(refreshProps: OnRefreshProps) => {
updateTimeRange(refreshProps);
setLastReloadTime(Date.now());
},
[updateTimeRange]
);
return (
<>
{isNonAggregatable && (
<EuiFlexGroup
data-test-subj="datasetQualityFlyoutNonAggregatableWarning"
style={{ marginBottom: '24px' }}
>
<EuiFlexItem>
<EuiCallOut title={nonAggregatableWarningTitle} color="warning" iconType="warning">
<p>{nonAggregatableWarningDescription(dataStream)}</p>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
)}
<FlyoutSummaryHeader
timeRange={timeRange}
onTimeChange={handleTimeChange}
onRefresh={handleRefresh}
/>
<EuiSpacer size="m" />
{dataStreamStat ? (
<FlyoutSummaryKpis
dataStreamStat={dataStreamStat}
dataStreamDetails={dataStreamDetails}
isLoading={dataStreamDetailsLoading}
timeRange={timeRange}
/>
) : (
<FlyoutSummaryKpisLoading />
)}
<EuiSpacer />
<DegradedDocs
dataStream={dataStream}
timeRange={timeRange}
lastReloadTime={lastReloadTime}
onTimeRangeChange={handleTimeRangeChange}
/>
<EuiSpacer />
<DegradedFields />
</>
);
}

View file

@ -1,78 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { css } from '@emotion/react';
import {
EuiFlexGroup,
EuiIcon,
EuiSuperDatePicker,
EuiTitle,
EuiToolTip,
OnRefreshProps,
OnTimeChangeProps,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { flyoutSummaryText } from '../../../../common/translations';
import { TimeRangeConfig } from '../../../../common/types';
export function FlyoutSummaryHeader({
timeRange,
onTimeChange,
onRefresh,
}: {
timeRange: TimeRangeConfig;
onTimeChange: (timeChangeProps: OnTimeChangeProps) => void;
onRefresh: (refreshProps: OnRefreshProps) => void;
}) {
return (
<EuiFlexGroup alignItems="center" wrap={true}>
<EuiFlexGroup
css={css`
flex-grow: 1;
`}
justifyContent="flexStart"
alignItems="center"
gutterSize="xs"
>
<EuiTitle size="s">
<span>{flyoutSummaryText}</span>
</EuiTitle>
<EuiToolTip content={flyoutSummaryTooltip}>
<EuiIcon size="m" color="subdued" type="questionInCircle" className="eui-alignTop" />
</EuiToolTip>
</EuiFlexGroup>
<EuiFlexGroup
css={css`
flex-grow: 0;
`}
>
<EuiSuperDatePicker
width="auto"
compressed={true}
isLoading={false}
start={timeRange.from}
end={timeRange.to}
onTimeChange={onTimeChange}
onRefresh={onRefresh}
isQuickSelectOnly={false}
showUpdateButton="iconOnly"
updateButtonProps={{ fill: false }}
/>
</EuiFlexGroup>
</EuiFlexGroup>
);
}
const flyoutSummaryTooltip = (
<FormattedMessage
id="xpack.datasetQuality.flyoutSummaryTooltip"
defaultMessage="Stats of the data set within the selected time range."
/>
);

View file

@ -1,107 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import {
EuiFlexGroup,
EuiFlexItem,
EuiPanel,
EuiTitle,
EuiText,
EuiLink,
useEuiTheme,
EuiSkeletonTitle,
EuiSkeletonRectangle,
} from '@elastic/eui';
import { PrivilegesWarningIconWrapper } from '../../common';
import { notAvailableLabel } from '../../../../common/translations';
import type { getSummaryKpis } from './get_summary_kpis';
export function FlyoutSummaryKpiItem({
title,
value,
link,
isLoading,
userHasPrivilege,
}: ReturnType<typeof getSummaryKpis>[number] & { isLoading: boolean }) {
const { euiTheme } = useEuiTheme();
return (
<EuiPanel
data-test-subj={`datasetQualityFlyoutKpi-${title}${isLoading ? '--loading' : ''}`}
css={{ minWidth: 152, height: 130, display: 'flex', alignItems: 'stretch' }}
hasBorder
grow={false}
paddingSize="s"
>
<EuiFlexGroup alignItems="stretch" direction="column" wrap={false}>
<EuiFlexItem css={{ gap: euiTheme.size.xs }}>
<EuiFlexGroup>
<EuiTitle data-test-subj={`datasetQualityFlyoutKpiTitle-${title}`} size="xxxs">
<h6>{title}</h6>
</EuiTitle>
<PrivilegesWarningIconWrapper
hasPrivileges={userHasPrivilege}
title={title}
mode="popover"
popoverCss={{ marginLeft: 'auto' }}
>
<></>
</PrivilegesWarningIconWrapper>
</EuiFlexGroup>
{link ? (
<EuiLink
data-test-subj={`datasetQualityFlyoutKpiLink-${title}`}
css={{
display: 'flex',
alignItems: 'center',
width: 'fit-content',
}}
{...link.props}
>
<EuiText
css={{
fontWeight: euiTheme.font.weight.semiBold,
whiteSpace: 'nowrap',
}}
size="xs"
>
{link.label}
</EuiText>
</EuiLink>
) : null}
</EuiFlexItem>
<EuiFlexItem
css={{ alignItems: isLoading ? 'stretch' : 'flex-end', justifyContent: 'flex-end' }}
>
<EuiSkeletonTitle
style={{ width: '50%', marginLeft: 'auto' }}
size="m"
isLoading={isLoading}
>
<EuiTitle data-test-subj={`datasetQualityFlyoutKpiValue-${title}`} size="s">
<h3 className="eui-textNoWrap">{userHasPrivilege ? value : notAvailableLabel}</h3>
</EuiTitle>
</EuiSkeletonTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}
export function FlyoutSummaryKpiItemLoading({ title }: { title: string }) {
return (
<EuiSkeletonRectangle
data-test-subj={`datasetQualityFlyoutKpi-${title}--loading`}
css={{ minWidth: 152 }}
width={'100%'}
height={130}
/>
);
}

View file

@ -1,88 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { _IGNORED } from '../../../../common/es_fields';
import { DataStreamDetails } from '../../../../common/api_types';
import { useKibanaContextForPlugin } from '../../../utils';
import { NavigationSource } from '../../../services/telemetry';
import { useDatasetDetailsTelemetry, useRedirectLink } from '../../../hooks';
import { FlyoutDataset } from '../../../state_machines/dataset_quality_controller';
import { FlyoutSummaryKpiItem, FlyoutSummaryKpiItemLoading } from './flyout_summary_kpi_item';
import { getSummaryKpis } from './get_summary_kpis';
import { TimeRangeConfig } from '../../../../common/types';
export function FlyoutSummaryKpis({
dataStreamStat,
dataStreamDetails,
isLoading,
timeRange,
}: {
dataStreamStat: FlyoutDataset;
dataStreamDetails?: DataStreamDetails;
isLoading: boolean;
timeRange: TimeRangeConfig;
}) {
const {
services: { observabilityShared },
} = useKibanaContextForPlugin();
const telemetry = useDatasetDetailsTelemetry();
const hostsLocator = observabilityShared.locators.infra.hostsLocator;
const degradedDocsLinkProps = useRedirectLink({
dataStreamStat,
query: { language: 'kuery', query: `${_IGNORED}: *` },
timeRangeConfig: timeRange,
telemetry: {
page: 'details',
navigationSource: NavigationSource.Summary,
},
});
const kpis = useMemo(
() =>
getSummaryKpis({
dataStreamDetails,
timeRange,
degradedDocsLinkProps,
hostsLocator,
telemetry,
}),
[dataStreamDetails, degradedDocsLinkProps, hostsLocator, telemetry, timeRange]
);
return (
<EuiFlexGroup direction="column">
<EuiFlexGroup wrap={true} gutterSize="m">
{kpis.map((kpi) => (
<EuiFlexItem key={kpi.title}>
<FlyoutSummaryKpiItem {...kpi} isLoading={isLoading} />
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexGroup>
);
}
export function FlyoutSummaryKpisLoading() {
const telemetry = useDatasetDetailsTelemetry();
return (
<EuiFlexGroup direction="column">
<EuiFlexGroup wrap={true} gutterSize="m">
{getSummaryKpis({ telemetry }).map(({ title }) => (
<EuiFlexItem key={title}>
<FlyoutSummaryKpiItemLoading title={title} />
</EuiFlexItem>
))}
</EuiFlexGroup>
</EuiFlexGroup>
);
}

View file

@ -1,171 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { formatNumber } from '@elastic/eui';
import type { useKibanaContextForPlugin } from '../../../utils';
import type { useDatasetDetailsTelemetry } from '../../../hooks';
import {
BYTE_NUMBER_FORMAT,
DEFAULT_DATEPICKER_REFRESH,
DEFAULT_TIME_RANGE,
MAX_HOSTS_METRIC_VALUE,
} from '../../../../common/constants';
import {
overviewDegradedDocsText,
flyoutDocsCountTotalText,
flyoutHostsText,
flyoutServicesText,
flyoutShowAllText,
flyoutSizeText,
} from '../../../../common/translations';
import { getSummaryKpis } from './get_summary_kpis';
import { TimeRangeConfig } from '../../../../common/types';
const dataStreamDetails = {
services: {
service1: ['service1Instance1', 'service1Instance2'],
service2: ['service2Instance1'],
},
docsCount: 1000,
sizeBytes: 5000,
hosts: {
host1: ['host1Instance1', 'host1Instance2'],
host2: ['host2Instance1'],
},
degradedDocsCount: 200,
};
const timeRange: TimeRangeConfig = {
...DEFAULT_TIME_RANGE,
refresh: DEFAULT_DATEPICKER_REFRESH,
from: 'now-15m',
to: 'now',
};
const degradedDocsLinkProps = {
linkProps: { href: 'http://exploratory-view/degraded-docs', onClick: () => {} },
navigate: () => {},
isLogsExplorerAvailable: true,
};
const hostsRedirectUrl = 'http://hosts/metric/';
const hostsLocator = {
getRedirectUrl: () => hostsRedirectUrl,
} as unknown as ReturnType<
typeof useKibanaContextForPlugin
>['services']['observabilityShared']['locators']['infra']['hostsLocator'];
const telemetry = {
trackDetailsNavigated: () => {},
} as unknown as ReturnType<typeof useDatasetDetailsTelemetry>;
describe('getSummaryKpis', () => {
it('should return the correct KPIs', () => {
const result = getSummaryKpis({
dataStreamDetails,
timeRange,
degradedDocsLinkProps,
hostsLocator,
telemetry,
});
expect(result).toEqual([
{
title: flyoutDocsCountTotalText,
value: '1,000',
userHasPrivilege: true,
},
{
title: flyoutSizeText,
value: formatNumber(dataStreamDetails.sizeBytes ?? 0, BYTE_NUMBER_FORMAT),
userHasPrivilege: false,
},
{
title: flyoutServicesText,
value: '3',
link: undefined,
userHasPrivilege: true,
},
{
title: flyoutHostsText,
value: '3',
link: undefined,
userHasPrivilege: true,
},
{
title: overviewDegradedDocsText,
value: '200',
link: {
label: flyoutShowAllText,
props: degradedDocsLinkProps.linkProps,
},
userHasPrivilege: true,
},
]);
});
it('show X+ if number of hosts or services exceed MAX_HOSTS_METRIC_VALUE', () => {
const services = {
service1: new Array(MAX_HOSTS_METRIC_VALUE + 1)
.fill('service1Instance')
.map((_, i) => `service1Instance${i}`),
};
const host3 = new Array(MAX_HOSTS_METRIC_VALUE + 1)
.fill('host3Instance')
.map((_, i) => `host3Instance${i}`);
const detailsWithMaxPlusHosts = {
...dataStreamDetails,
services,
hosts: { ...dataStreamDetails.hosts, host3 },
};
const result = getSummaryKpis({
dataStreamDetails: detailsWithMaxPlusHosts,
timeRange,
degradedDocsLinkProps,
hostsLocator,
telemetry,
});
expect(result).toEqual([
{
title: flyoutDocsCountTotalText,
value: '1,000',
userHasPrivilege: true,
},
{
title: flyoutSizeText,
value: formatNumber(dataStreamDetails.sizeBytes ?? 0, BYTE_NUMBER_FORMAT),
userHasPrivilege: false,
},
{
title: flyoutServicesText,
value: '50+',
link: undefined,
userHasPrivilege: true,
},
{
title: flyoutHostsText,
value: '54+',
link: undefined,
userHasPrivilege: true,
},
{
title: overviewDegradedDocsText,
value: '200',
link: {
label: flyoutShowAllText,
props: degradedDocsLinkProps.linkProps,
},
userHasPrivilege: true,
},
]);
});
});

View file

@ -1,167 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { formatNumber } from '@elastic/eui';
import { getRouterLinkProps, RouterLinkProps } from '@kbn/router-utils/src/get_router_link_props';
import {
BYTE_NUMBER_FORMAT,
DEFAULT_DATEPICKER_REFRESH,
DEFAULT_TIME_RANGE,
MAX_HOSTS_METRIC_VALUE,
NUMBER_FORMAT,
} from '../../../../common/constants';
import {
overviewDegradedDocsText,
flyoutDocsCountTotalText,
flyoutHostsText,
flyoutServicesText,
flyoutShowAllText,
flyoutSizeText,
} from '../../../../common/translations';
import { DataStreamDetails } from '../../../../common/api_types';
import { NavigationTarget, NavigationSource } from '../../../services/telemetry';
import { useKibanaContextForPlugin } from '../../../utils';
import type { useRedirectLink, useDatasetDetailsTelemetry } from '../../../hooks';
import { TimeRangeConfig } from '../../../../common/types';
export function getSummaryKpis({
dataStreamDetails,
timeRange = { ...DEFAULT_TIME_RANGE, refresh: DEFAULT_DATEPICKER_REFRESH },
degradedDocsLinkProps,
hostsLocator,
telemetry,
}: {
dataStreamDetails?: DataStreamDetails;
timeRange?: TimeRangeConfig;
degradedDocsLinkProps?: ReturnType<typeof useRedirectLink>;
hostsLocator?: ReturnType<
typeof useKibanaContextForPlugin
>['services']['observabilityShared']['locators']['infra']['hostsLocator'];
telemetry: ReturnType<typeof useDatasetDetailsTelemetry>;
}): Array<{
title: string;
value: string;
link?: { label: string; props: RouterLinkProps };
userHasPrivilege: boolean;
}> {
const services = dataStreamDetails?.services ?? {};
const serviceKeys = Object.keys(services);
const countOfServices = serviceKeys
.map((key: string) => services[key].length)
.reduce((a, b) => a + b, 0);
// @ts-ignore // TODO: Add link to APM services page when possible - https://github.com/elastic/kibana/issues/179904
const servicesLink = {
label: flyoutShowAllText,
props: getRouterLinkProps({
href: undefined,
onClick: () => {
telemetry.trackDetailsNavigated(NavigationTarget.Services, NavigationSource.Summary);
},
}),
};
return [
{
title: flyoutDocsCountTotalText,
value: formatNumber(dataStreamDetails?.docsCount ?? 0, NUMBER_FORMAT),
userHasPrivilege: true,
},
// dataStreamDetails.sizeBytes = null indicates it's Serverless where `_stats` API isn't available
...(dataStreamDetails?.sizeBytes !== null // Only show when not in Serverless
? [
{
title: flyoutSizeText,
value: formatNumber(dataStreamDetails?.sizeBytes ?? 0, BYTE_NUMBER_FORMAT),
userHasPrivilege: Boolean(dataStreamDetails?.userPrivileges?.canMonitor),
},
]
: []),
{
title: flyoutServicesText,
value: formatMetricValueForMax(countOfServices, MAX_HOSTS_METRIC_VALUE, NUMBER_FORMAT),
link: undefined,
userHasPrivilege: true,
},
getHostsKpi(dataStreamDetails?.hosts, timeRange, telemetry, hostsLocator),
{
title: overviewDegradedDocsText,
value: formatNumber(dataStreamDetails?.degradedDocsCount ?? 0, NUMBER_FORMAT),
link:
degradedDocsLinkProps && degradedDocsLinkProps.linkProps.href
? {
label: flyoutShowAllText,
props: degradedDocsLinkProps.linkProps,
}
: undefined,
userHasPrivilege: true,
},
];
}
function getHostsKpi(
dataStreamHosts: DataStreamDetails['hosts'],
timeRange: TimeRangeConfig,
telemetry: ReturnType<typeof useDatasetDetailsTelemetry>,
hostsLocator?: ReturnType<
typeof useKibanaContextForPlugin
>['services']['observabilityShared']['locators']['infra']['hostsLocator']
) {
const hosts = dataStreamHosts ?? {};
const hostKeys = Object.keys(hosts);
const countOfHosts = hostKeys
.map((key: string) => hosts[key].length)
.reduce(
({ count, anyHostExceedsMax }, hostCount) => ({
count: count + hostCount,
anyHostExceedsMax: anyHostExceedsMax || hostCount > MAX_HOSTS_METRIC_VALUE,
}),
{ count: 0, anyHostExceedsMax: false }
);
// Create a query so from hostKeys so that (key: value OR key: value2)
const hostsKuery = hostKeys
.filter((key) => hosts[key].length > 0)
.map((key) => hosts[key].map((value) => `${key}: "${value}"`).join(' OR '))
.join(' OR ');
const hostsUrl = hostsLocator?.getRedirectUrl({
query: { language: 'kuery', query: hostsKuery },
dateRange: { from: timeRange.from, to: timeRange.to },
limit: countOfHosts.count,
});
// @ts-ignore // TODO: Add link to Infra Hosts page when possible
const hostsLink = {
label: flyoutShowAllText,
props: getRouterLinkProps({
href: hostsUrl,
onClick: () => {
telemetry.trackDetailsNavigated(NavigationTarget.Hosts, NavigationSource.Summary);
},
}),
};
return {
title: flyoutHostsText,
value: formatMetricValueForMax(
countOfHosts.anyHostExceedsMax ? countOfHosts.count + 1 : countOfHosts.count,
countOfHosts.count,
NUMBER_FORMAT
),
link: undefined,
userHasPrivilege: true,
};
}
/**
* Formats a metric value to show a '+' sign if it's above a max value e.g. 50+
*/
function formatMetricValueForMax(value: number, max: number, numberFormat: string): string {
const exceedsMax = value > max;
const valueToShow = exceedsMax ? max : value;
return `${formatNumber(valueToShow, numberFormat)}${exceedsMax ? '+' : ''}`;
}

View file

@ -1,102 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiButton,
EuiFlexGroup,
EuiFlexItem,
EuiFlyoutHeader,
EuiSkeletonTitle,
EuiTitle,
useEuiShadow,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import React from 'react';
import { openInDiscoverText, openInLogsExplorerText } from '../../../common/translations';
import { NavigationSource } from '../../services/telemetry';
import { useRedirectLink } from '../../hooks';
import { IntegrationIcon } from '../common';
import { BasicDataStream, TimeRangeConfig } from '../../../common/types';
export function Header({
linkDetails,
loading,
title,
timeRange,
}: {
linkDetails: BasicDataStream;
loading: boolean;
title: string;
timeRange: TimeRangeConfig;
}) {
const { integration } = linkDetails;
const euiShadow = useEuiShadow('s');
const { euiTheme } = useEuiTheme();
const redirectLinkProps = useRedirectLink({
dataStreamStat: linkDetails,
telemetry: {
page: 'details',
navigationSource: NavigationSource.Header,
},
timeRangeConfig: timeRange,
});
return (
<EuiFlyoutHeader hasBorder>
{loading ? (
<EuiSkeletonTitle
size="s"
data-test-subj="datasetQualityFlyoutIntegrationLoading"
className="datasetQualityFlyoutIntegrationLoading"
/>
) : (
<EuiFlexGroup justifyContent="flexStart">
<EuiFlexItem grow>
<EuiFlexGroup gutterSize="m" justifyContent="flexStart" alignItems="center">
<EuiTitle data-test-subj="datasetQualityFlyoutTitle">
<h3>{title}</h3>
</EuiTitle>
<div
css={css`
${euiShadow};
padding: ${euiTheme.size.xs};
border-radius: ${euiTheme.size.xxs};
`}
>
<IntegrationIcon integration={integration} />
</div>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup
css={css`
margin-right: ${euiTheme.size.l};
`}
gutterSize="s"
justifyContent="flexEnd"
alignItems="center"
>
<EuiButton
data-test-subj="datasetQualityHeaderButton"
size="s"
{...redirectLinkProps.linkProps}
iconType={
redirectLinkProps.isLogsExplorerAvailable ? 'logoObservability' : 'discoverApp'
}
>
{redirectLinkProps.isLogsExplorerAvailable
? openInLogsExplorerText
: openInDiscoverText}
</EuiButton>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
)}
</EuiFlyoutHeader>
);
}

View file

@ -1,8 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './flyout';

View file

@ -1,204 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import {
EuiButtonEmpty,
EuiButtonIcon,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
EuiPopover,
EuiSkeletonRectangle,
} from '@elastic/eui';
import { css } from '@emotion/react';
import { RouterLinkProps } from '@kbn/router-utils/src/get_router_link_props';
import { useDatasetQualityFlyout } from '../../hooks';
import { useFlyoutIntegrationActions } from '../../hooks/use_flyout_integration_actions';
import { Integration } from '../../../common/data_streams_stats/integration';
import { Dashboard } from '../../../common/api_types';
const integrationActionsText = i18n.translate('xpack.datasetQuality.flyoutIntegrationActionsText', {
defaultMessage: 'Integration actions',
});
const seeIntegrationText = i18n.translate('xpack.datasetQuality.flyoutSeeIntegrationActionText', {
defaultMessage: 'See integration',
});
const indexTemplateText = i18n.translate('xpack.datasetQuality.flyoutIndexTemplateActionText', {
defaultMessage: 'Index template',
});
const viewDashboardsText = i18n.translate('xpack.datasetQuality.flyoutViewDashboardsActionText', {
defaultMessage: 'View dashboards',
});
export function IntegrationActionsMenu({
integration,
dashboards,
dashboardsLoading,
}: {
integration: Integration;
dashboards: Dashboard[];
dashboardsLoading: boolean;
}) {
const { dataStreamStat, canUserAccessDashboards, canUserViewIntegrations } =
useDatasetQualityFlyout();
const { version, name: integrationName } = integration;
const { type, name } = dataStreamStat!;
const {
isOpen,
handleCloseMenu,
handleToggleMenu,
getIntegrationOverviewLinkProps,
getIndexManagementLinkProps,
getDashboardLinkProps,
} = useFlyoutIntegrationActions();
const actionButton = (
<EuiButtonIcon
title={integrationActionsText}
aria-label={integrationActionsText}
iconType="boxesHorizontal"
onClick={handleToggleMenu}
data-test-subj="datasetQualityFlyoutIntegrationActionsButton"
/>
);
const MenuActionItem = ({
dataTestSubject,
buttonText,
routerLinkProps,
iconType,
disabled = false,
}: {
dataTestSubject: string;
buttonText: string | React.ReactNode;
routerLinkProps: RouterLinkProps;
iconType: string;
disabled?: boolean;
}) => (
<EuiButtonEmpty
{...routerLinkProps}
size="s"
css={css`
font-weight: normal;
`}
color="text"
iconType={iconType}
data-test-subj={dataTestSubject}
disabled={disabled}
>
{buttonText}
</EuiButtonEmpty>
);
const panelItems = useMemo(() => {
const firstLevelItems: EuiContextMenuPanelItemDescriptor[] = [
...(canUserViewIntegrations
? [
{
renderItem: () => (
<MenuActionItem
buttonText={seeIntegrationText}
dataTestSubject="datasetQualityFlyoutIntegrationActionOverview"
routerLinkProps={getIntegrationOverviewLinkProps(integrationName, version)}
iconType="package"
disabled={!canUserViewIntegrations}
/>
),
},
]
: []),
{
renderItem: () => (
<MenuActionItem
buttonText={indexTemplateText}
dataTestSubject="datasetQualityFlyoutIntegrationActionTemplate"
routerLinkProps={getIndexManagementLinkProps({
sectionId: 'data',
appId: `index_management/templates/${type}-${name}`,
})}
iconType="indexPatternApp"
/>
),
},
{
isSeparator: true,
key: 'sep',
},
];
if (dashboards.length && canUserAccessDashboards) {
firstLevelItems.push({
icon: 'dashboardApp',
panel: 1,
name: viewDashboardsText,
'data-test-subj': 'datasetQualityFlyoutIntegrationActionViewDashboards',
disabled: false,
});
} else if (dashboardsLoading) {
firstLevelItems.push({
icon: 'dashboardApp',
name: <EuiSkeletonRectangle width={120} title={viewDashboardsText} />,
'data-test-subj': 'datasetQualityFlyoutIntegrationActionDashboardsLoading',
disabled: true,
});
}
const panel: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
items: firstLevelItems,
},
{
id: 1,
title: viewDashboardsText,
items: dashboards.map((dashboard) => {
return {
renderItem: () => (
<MenuActionItem
buttonText={dashboard.title}
dataTestSubject="datasetQualityFlyoutIntegrationActionDashboard"
routerLinkProps={getDashboardLinkProps(dashboard)}
iconType="dashboardApp"
/>
),
};
}),
},
];
return panel;
}, [
dashboards,
getDashboardLinkProps,
getIndexManagementLinkProps,
getIntegrationOverviewLinkProps,
integrationName,
name,
type,
version,
dashboardsLoading,
canUserAccessDashboards,
canUserViewIntegrations,
]);
return (
<EuiPopover
anchorPosition="downRight"
panelPaddingSize="none"
button={actionButton}
isOpen={isOpen}
closePopover={handleCloseMenu}
>
<EuiContextMenu size="s" panels={panelItems} initialPanelId={0} />
</EuiPopover>
);
}

View file

@ -1,72 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiBadge, EuiText } from '@elastic/eui';
import React from 'react';
import { css } from '@emotion/react';
import {
flyoutIntegrationDetailsText,
flyoutIntegrationNameText,
integrationVersionText,
} from '../../../common/translations';
import { IntegrationIcon } from '../common';
import { FieldsList } from './fields_list';
import { IntegrationActionsMenu } from './integration_actions_menu';
import { Integration } from '../../../common/data_streams_stats/integration';
import { Dashboard } from '../../../common/api_types';
// Allow for lazy loading
// eslint-disable-next-line import/no-default-export
export default function IntegrationSummary({
integration,
dashboards,
dashboardsLoading,
}: {
integration: Integration;
dashboards: Dashboard[];
dashboardsLoading: boolean;
}) {
const { name, version } = integration;
const integrationActionsMenu = (
<IntegrationActionsMenu
integration={integration}
dashboards={dashboards}
dashboardsLoading={dashboardsLoading}
/>
);
return (
<FieldsList
title={flyoutIntegrationDetailsText}
actionsMenu={integrationActionsMenu}
fields={[
{
fieldTitle: flyoutIntegrationNameText,
fieldValue: (
<EuiBadge
color="hollow"
css={css`
width: fit-content;
`}
>
<EuiFlexGroup gutterSize="xs" alignItems="center">
<IntegrationIcon integration={integration} />
<EuiText size="s">{name}</EuiText>
</EuiFlexGroup>
</EuiBadge>
),
isLoading: false,
},
{
fieldTitle: integrationVersionText,
fieldValue: version,
isLoading: false,
},
]}
/>
);
}

View file

@ -1,13 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; 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;
}

View file

@ -11,12 +11,10 @@ import equal from 'fast-deep-equal';
import { distinctUntilChanged, from, map } from 'rxjs';
import { interpret } from 'xstate';
import { IDataStreamsStatsClient } from '../../services/data_streams_stats';
import { IDataStreamDetailsClient } from '../../services/data_stream_details';
import {
createDatasetQualityControllerStateMachine,
DEFAULT_CONTEXT,
} from '../../state_machines/dataset_quality_controller';
import { DatasetQualityStartDeps } from '../../types';
import { getContextFromPublicState, getPublicStateFromContext } from './public_state';
import { DatasetQualityController, DatasetQualityPublicStateUpdate } from './types';
@ -24,13 +22,11 @@ type InitialState = DatasetQualityPublicStateUpdate;
interface Dependencies {
core: CoreStart;
plugins: DatasetQualityStartDeps;
dataStreamStatsClient: IDataStreamsStatsClient;
dataStreamDetailsClient: IDataStreamDetailsClient;
}
export const createDatasetQualityControllerFactory =
({ core, plugins, dataStreamStatsClient, dataStreamDetailsClient }: Dependencies) =>
({ core, dataStreamStatsClient }: Dependencies) =>
async ({
initialState = DEFAULT_CONTEXT,
}: {
@ -40,10 +36,8 @@ export const createDatasetQualityControllerFactory =
const machine = createDatasetQualityControllerStateMachine({
initialContext,
plugins,
toasts: core.notifications.toasts,
dataStreamStatsClient,
dataStreamDetailsClient,
});
const service = interpret(machine, {

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { DatasetTableSortField, DegradedFieldSortField } from '../../hooks';
import { DatasetTableSortField } from '../../hooks';
import {
DatasetQualityControllerContext,
DEFAULT_CONTEXT,
@ -17,7 +17,6 @@ export const getPublicStateFromContext = (
): DatasetQualityPublicState => {
return {
table: context.table,
flyout: context.flyout,
filters: context.filters,
};
};
@ -36,22 +35,6 @@ export const getContextFromPublicState = (
}
: DEFAULT_CONTEXT.table.sort,
},
flyout: {
...DEFAULT_CONTEXT.flyout,
...publicState.flyout,
degradedFields: {
table: {
...DEFAULT_CONTEXT.flyout.degradedFields.table,
...publicState.flyout?.degradedFields?.table,
sort: publicState.flyout?.degradedFields?.table?.sort
? {
...publicState.flyout.degradedFields.table.sort,
field: publicState.flyout.degradedFields.table.sort.field as DegradedFieldSortField,
}
: DEFAULT_CONTEXT.flyout.degradedFields.table.sort,
},
},
},
filters: {
...DEFAULT_CONTEXT.filters,
...publicState.filters,

View file

@ -9,9 +9,7 @@ import { Observable } from 'rxjs';
import {
DatasetQualityControllerStateService,
WithFilters,
WithFlyoutOptions,
WithTableOptions,
DegradedFields,
} from '../../state_machines/dataset_quality_controller';
export interface DatasetQualityController {
@ -25,25 +23,10 @@ export type DatasetQualityTableOptions = Partial<
Omit<WithTableOptions['table'], 'sort'> & { sort: TableSortOptions }
>;
type DegradedFieldSortOptions = Omit<DegradedFields['table']['sort'], 'field'> & { field: string };
export type DatasetQualityDegradedFieldTableOptions = Partial<
Omit<DegradedFields['table'], 'sort'> & {
sort: DegradedFieldSortOptions;
}
>;
export type DatasetQualityFlyoutOptions = Partial<
Omit<WithFlyoutOptions['flyout'], 'datasetDetails' | 'degradedFields'> & {
degradedFields: { table?: DatasetQualityDegradedFieldTableOptions };
}
>;
export type DatasetQualityFilterOptions = Partial<WithFilters['filters']>;
export interface DatasetQualityPublicState {
table: DatasetQualityTableOptions;
flyout: DatasetQualityFlyoutOptions;
filters: DatasetQualityFilterOptions;
}

View file

@ -28,7 +28,7 @@ export type DatasetQualityDetailsPublicState = WithDefaultControllerState;
// a must and everything else can be optional. The table inside the
// degradedFields must accept field property as string
export type DatasetQualityDetailsPublicStateUpdate = Partial<
Pick<WithDefaultControllerState, 'timeRange'>
Pick<WithDefaultControllerState, 'timeRange' | 'breakdownField'>
> & {
dataStream: string;
} & {

View file

@ -6,14 +6,13 @@
*/
export * from './use_dataset_quality_table';
export * from './use_dataset_quality_flyout';
export * from './use_degraded_docs_chart';
export * from './use_redirect_link';
export * from './use_summary_panel';
export * from './use_create_dataview';
export * from './use_dataset_quality_degraded_field';
export * from './use_telemetry';
export * from './use_redirect_link_telemetry';
export * from './use_dataset_quality_details_state';
export * from './use_dataset_quality_details_redirect_link';
export * from './use_degraded_fields';
export * from './use_integration_actions';
export * from './use_dataset_telemetry';
export * from './use_dataset_details_telemetry';

View file

@ -0,0 +1,200 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useCallback, useEffect, useMemo } from 'react';
import { RouterLinkProps } from '@kbn/router-utils/src/get_router_link_props';
import { getDateISORange } from '@kbn/timerange';
import { useDatasetQualityDetailsState } from './use_dataset_quality_details_state';
import { DatasetDetailsEbtProps, NavigationSource, NavigationTarget } from '../services/telemetry';
import { BasicDataStream, TimeRangeConfig } from '../../common/types';
import { DataStreamDetails } from '../../common/api_types';
import { Integration } from '../../common/data_streams_stats/integration';
import { mapPercentageToQuality } from '../../common/utils';
import { MASKED_FIELD_PLACEHOLDER, UNKOWN_FIELD_PLACEHOLDER } from '../../common/constants';
export function useDatasetDetailsTelemetry() {
const {
telemetryClient,
datasetDetails,
dataStreamDetails,
timeRange,
canUserViewIntegrations,
canUserAccessDashboards,
breakdownField,
isNonAggregatable,
isBreakdownFieldEcs,
integrationDetails,
loadingState,
} = useDatasetQualityDetailsState();
const ebtProps = useMemo<DatasetDetailsEbtProps | undefined>(() => {
if (dataStreamDetails && timeRange && !loadingState.dataStreamDetailsLoading) {
return getDatasetDetailsEbtProps({
datasetDetails,
dataStreamDetails,
timeRange,
canUserViewIntegrations,
canUserAccessDashboards,
breakdownField,
isNonAggregatable,
isBreakdownFieldEcs,
integration: integrationDetails.integration,
});
}
return undefined;
}, [
dataStreamDetails,
timeRange,
loadingState.dataStreamDetailsLoading,
datasetDetails,
canUserViewIntegrations,
canUserAccessDashboards,
breakdownField,
isNonAggregatable,
isBreakdownFieldEcs,
integrationDetails.integration,
]);
const startTracking = useCallback(() => {
telemetryClient.startDatasetDetailsTracking();
}, [telemetryClient]);
// Report opening dataset details
useEffect(() => {
const datasetDetailsTrackingState = telemetryClient.getDatasetDetailsTrackingState();
if (datasetDetailsTrackingState === 'started' && ebtProps) {
telemetryClient.trackDatasetDetailsOpened(ebtProps);
}
}, [ebtProps, telemetryClient]);
const trackDetailsNavigated = useCallback(
(target: NavigationTarget, source: NavigationSource, isDegraded = false) => {
const datasetDetailsTrackingState = telemetryClient.getDatasetDetailsTrackingState();
if (
(datasetDetailsTrackingState === 'opened' || datasetDetailsTrackingState === 'navigated') &&
ebtProps
) {
telemetryClient.trackDatasetDetailsNavigated({
...ebtProps,
filters: {
is_degraded: isDegraded,
},
target,
source,
});
} else {
throw new Error(
'Cannot report dataset details navigation telemetry without required data and state'
);
}
},
[ebtProps, telemetryClient]
);
const trackDatasetDetailsBreakdownFieldChanged = useCallback(() => {
const datasetDetailsTrackingState = telemetryClient.getDatasetDetailsTrackingState();
if (
(datasetDetailsTrackingState === 'opened' || datasetDetailsTrackingState === 'navigated') &&
ebtProps
) {
telemetryClient.trackDatasetDetailsBreakdownFieldChanged({
...ebtProps,
breakdown_field: ebtProps.breakdown_field,
});
}
}, [ebtProps, telemetryClient]);
const wrapLinkPropsForTelemetry = useCallback(
(
props: RouterLinkProps,
target: NavigationTarget,
source: NavigationSource,
isDegraded = false
) => {
return {
...props,
onClick: (event: Parameters<RouterLinkProps['onClick']>[0]) => {
trackDetailsNavigated(target, source, isDegraded);
if (props.onClick) {
props.onClick(event);
}
},
};
},
[trackDetailsNavigated]
);
return {
startTracking,
trackDetailsNavigated,
wrapLinkPropsForTelemetry,
navigationTargets: NavigationTarget,
navigationSources: NavigationSource,
trackDatasetDetailsBreakdownFieldChanged,
};
}
function getDatasetDetailsEbtProps({
datasetDetails,
dataStreamDetails,
timeRange,
canUserViewIntegrations,
canUserAccessDashboards,
breakdownField,
isNonAggregatable,
isBreakdownFieldEcs,
integration,
}: {
datasetDetails: BasicDataStream;
dataStreamDetails: DataStreamDetails;
timeRange: TimeRangeConfig;
canUserViewIntegrations: boolean;
canUserAccessDashboards: boolean;
breakdownField?: string;
isNonAggregatable: boolean;
isBreakdownFieldEcs: boolean;
integration?: Integration;
}): DatasetDetailsEbtProps {
const indexName = datasetDetails.rawName;
const dataStream = {
dataset: datasetDetails.name,
namespace: datasetDetails.namespace,
type: datasetDetails.type,
};
const degradedDocs = dataStreamDetails?.degradedDocsCount ?? 0;
const totalDocs = dataStreamDetails?.docsCount ?? 0;
const degradedPercentage =
totalDocs > 0 ? Number(((degradedDocs / totalDocs) * 100).toFixed(2)) : 0;
const health = mapPercentageToQuality(degradedPercentage);
const { startDate: from, endDate: to } = getDateISORange(timeRange);
return {
index_name: indexName,
data_stream: dataStream,
privileges: {
can_monitor_data_stream: true,
can_view_integrations: canUserViewIntegrations,
can_view_dashboards: canUserAccessDashboards,
},
data_stream_aggregatable: !isNonAggregatable,
data_stream_health: health,
from,
to,
degraded_percentage: degradedPercentage,
integration: integration?.name,
breakdown_field: breakdownField
? isBreakdownFieldEcs === null
? UNKOWN_FIELD_PLACEHOLDER
: getMaskedBreakdownField(breakdownField, isBreakdownFieldEcs)
: breakdownField,
};
}
function getMaskedBreakdownField(breakdownField: string, isBreakdownFieldEcs: boolean) {
return isBreakdownFieldEcs ? breakdownField : MASKED_FIELD_PLACEHOLDER;
}

View file

@ -1,76 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useSelector } from '@xstate/react';
import { useCallback, useMemo } from 'react';
import { orderBy } from 'lodash';
import { useDatasetQualityContext } from '../components/dataset_quality/context';
import { DegradedField } from '../../common/data_streams_stats';
import { SortDirection } from '../../common/types';
import {
DEFAULT_DEGRADED_FIELD_SORT_DIRECTION,
DEFAULT_DEGRADED_FIELD_SORT_FIELD,
} from '../../common/constants';
import { useKibanaContextForPlugin } from '../utils';
type DegradedFieldSortField = keyof DegradedField;
// TODO: DELETE this hook in favour of new hook post migration
export function useDatasetQualityDegradedField() {
const { service } = useDatasetQualityContext();
const {
services: { fieldFormats },
} = useKibanaContextForPlugin();
const degradedFields = useSelector(service, (state) => state.context.flyout.degradedFields) ?? {};
const { data, table } = degradedFields;
const { page, rowsPerPage, sort } = table;
const pagination = {
pageIndex: page,
pageSize: rowsPerPage,
totalItemCount: data?.length ?? 0,
hidePerPageOptions: true,
};
const onTableChange = useCallback(
(options: {
page: { index: number; size: number };
sort?: { field: DegradedFieldSortField; direction: SortDirection };
}) => {
service.send({
type: 'UPDATE_DEGRADED_FIELDS_TABLE_CRITERIA',
degraded_field_criteria: {
page: options.page.index,
rowsPerPage: options.page.size,
sort: {
field: options.sort?.field || DEFAULT_DEGRADED_FIELD_SORT_FIELD,
direction: options.sort?.direction || DEFAULT_DEGRADED_FIELD_SORT_DIRECTION,
},
},
});
},
[service]
);
const renderedItems = useMemo(() => {
const sortedItems = orderBy(data, sort.field, sort.direction);
return sortedItems.slice(page * rowsPerPage, (page + 1) * rowsPerPage);
}, [data, sort.field, sort.direction, page, rowsPerPage]);
const isLoading = useSelector(service, (state) =>
state.matches('flyout.initializing.dataStreamDegradedFields.fetching')
);
return {
isLoading,
pagination,
onTableChange,
renderedItems,
sort: { sort },
fieldFormats,
};
}

View file

@ -1,188 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
SINGLE_DATASET_LOCATOR_ID,
type SingleDatasetLocatorParams,
} from '@kbn/deeplinks-observability';
import { type DiscoverAppLocatorParams, DISCOVER_APP_LOCATOR } from '@kbn/discover-plugin/common';
import { type Query, type AggregateQuery, buildPhraseFilter } from '@kbn/es-query';
import { getRouterLinkProps } from '@kbn/router-utils';
import type { RouterLinkProps } from '@kbn/router-utils/src/get_router_link_props';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import type { LocatorClient } from '@kbn/shared-ux-prompt-no-data-views-types';
import { useKibanaContextForPlugin } from '../utils';
import { BasicDataStream, TimeRangeConfig } from '../../common/types';
export const useDatasetQualityDetailsRedirectLink = <T extends BasicDataStream>({
dataStreamStat,
query,
timeRangeConfig,
breakdownField,
}: {
dataStreamStat: T;
query?: Query | AggregateQuery;
timeRangeConfig: TimeRangeConfig;
breakdownField?: string;
}) => {
const {
services: { share },
} = useKibanaContextForPlugin();
const { from, to } = timeRangeConfig;
const logsExplorerLocator =
share.url.locators.get<SingleDatasetLocatorParams>(SINGLE_DATASET_LOCATOR_ID);
const config = logsExplorerLocator
? buildLogsExplorerConfig({
locator: logsExplorerLocator,
dataStreamStat,
query,
from,
to,
breakdownField,
})
: buildDiscoverConfig({
locatorClient: share.url.locators,
dataStreamStat,
query,
from,
to,
breakdownField,
});
return {
linkProps: {
...config.routerLinkProps,
},
navigate: config.navigate,
isLogsExplorerAvailable: !!logsExplorerLocator,
};
};
const buildLogsExplorerConfig = <T extends BasicDataStream>({
locator,
dataStreamStat,
query,
from,
to,
breakdownField,
}: {
locator: LocatorPublic<SingleDatasetLocatorParams>;
dataStreamStat: T;
query?: Query | AggregateQuery;
from: string;
to: string;
breakdownField?: string;
}): {
navigate: () => void;
routerLinkProps: RouterLinkProps;
} => {
const params: SingleDatasetLocatorParams = {
dataset: dataStreamStat.name,
timeRange: {
from,
to,
},
integration: dataStreamStat.integration?.name,
query,
filterControls: {
namespace: {
mode: 'include',
values: [dataStreamStat.namespace],
},
},
breakdownField,
};
const urlToLogsExplorer = locator.getRedirectUrl(params);
const navigateToLogsExplorer = () => {
locator.navigate(params) as Promise<void>;
};
const logsExplorerLinkProps = getRouterLinkProps({
href: urlToLogsExplorer,
onClick: navigateToLogsExplorer,
});
return { routerLinkProps: logsExplorerLinkProps, navigate: navigateToLogsExplorer };
};
const buildDiscoverConfig = <T extends BasicDataStream>({
locatorClient,
dataStreamStat,
query,
from,
to,
breakdownField,
}: {
locatorClient: LocatorClient;
dataStreamStat: T;
query?: Query | AggregateQuery;
from: string;
to: string;
breakdownField?: string;
}): {
navigate: () => void;
routerLinkProps: RouterLinkProps;
} => {
const dataViewId = `${dataStreamStat.type}-${dataStreamStat.name}-*`;
const dataViewTitle = dataStreamStat.integration
? `[${dataStreamStat.integration.title}] ${dataStreamStat.name}`
: `${dataViewId}`;
const params: DiscoverAppLocatorParams = {
timeRange: {
from,
to,
},
refreshInterval: {
pause: true,
value: 60000,
},
dataViewId,
dataViewSpec: {
id: dataViewId,
title: dataViewTitle,
},
query,
breakdownField,
columns: ['@timestamp', 'message'],
filters: [
buildPhraseFilter(
{
name: 'data_stream.namespace',
type: 'string',
},
dataStreamStat.namespace,
{
id: dataViewId,
title: dataViewTitle,
}
),
],
interval: 'auto',
sort: [['@timestamp', 'desc']],
};
const locator = locatorClient.get<DiscoverAppLocatorParams>(DISCOVER_APP_LOCATOR);
const urlToDiscover = locator?.getRedirectUrl(params);
const navigateToDiscover = () => {
locator?.navigate(params) as Promise<void>;
};
const discoverLinkProps = getRouterLinkProps({
href: urlToDiscover,
onClick: navigateToDiscover,
});
return { routerLinkProps: discoverLinkProps, navigate: navigateToDiscover };
};

View file

@ -15,7 +15,7 @@ import { BasicDataStream } from '../../common/types';
import { useKibanaContextForPlugin } from '../utils';
export const useDatasetQualityDetailsState = () => {
const { service } = useDatasetQualityDetailsContext();
const { service, telemetryClient } = useDatasetQualityDetailsContext();
const {
services: { fieldFormats },
@ -36,6 +36,14 @@ export const useDatasetQualityDetailsState = () => {
: false
);
const isBreakdownFieldAsserted = useSelector(
service,
(state) =>
state.matches('initializing.checkBreakdownFieldIsEcs.done') &&
breakdownField &&
isBreakdownFieldEcs
);
const dataStreamSettings = useSelector(service, (state) =>
state.matches('initializing.dataStreamSettings.initializeIntegrations')
? state.context.dataStreamSettings
@ -67,7 +75,9 @@ export const useDatasetQualityDetailsState = () => {
)
);
const canUserViewIntegrations = dataStreamSettings?.datasetUserPrivileges?.canViewIntegrations;
const canUserViewIntegrations = Boolean(
dataStreamSettings?.datasetUserPrivileges?.canViewIntegrations
);
const dataStreamDetails = useSelector(service, (state) =>
state.matches('initializing.dataStreamDetails.done')
@ -115,6 +125,7 @@ export const useDatasetQualityDetailsState = () => {
return {
service,
telemetryClient,
fieldFormats,
isIndexNotFoundError,
dataStream,
@ -123,6 +134,7 @@ export const useDatasetQualityDetailsState = () => {
dataStreamDetails,
breakdownField,
isBreakdownFieldEcs,
isBreakdownFieldAsserted,
isNonAggregatable,
timeRange,
loadingState,

View file

@ -1,69 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useSelector } from '@xstate/react';
import { useDatasetQualityContext } from '../components/dataset_quality/context';
import { useKibanaContextForPlugin } from '../utils';
export const useDatasetQualityFlyout = () => {
const {
services: { fieldFormats },
} = useKibanaContextForPlugin();
const { service } = useDatasetQualityContext();
const {
dataset: dataStreamStat,
dataStreamSettings,
datasetDetails: dataStreamDetails,
insightsTimeRange,
breakdownField,
isNonAggregatable,
integration,
} = useSelector(service, (state) => state.context.flyout) ?? {};
const { timeRange } = useSelector(service, (state) => state.context.filters);
const loadingState = useSelector(service, (state) => ({
dataStreamDetailsLoading: state.matches('flyout.initializing.dataStreamDetails.fetching'),
dataStreamSettingsLoading: state.matches('flyout.initializing.dataStreamSettings.fetching'),
datasetIntegrationDashboardLoading: state.matches(
'flyout.initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.fetching'
),
datasetIntegrationDone: state.matches(
'flyout.initializing.dataStreamSettings.initializeIntegrations.integrationDetails.done'
),
}));
const canUserAccessDashboards = useSelector(
service,
(state) =>
!state.matches(
'flyout.initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.unauthorized'
)
);
const canUserViewIntegrations = useSelector(
service,
(state) => state.context.datasetUserPrivileges.canViewIntegrations
);
return {
dataStreamStat,
dataStreamSettings,
dataStreamDetails,
isNonAggregatable,
integration,
fieldFormats,
timeRange: insightsTimeRange ?? timeRange,
breakdownField,
loadingState,
flyoutLoading: !dataStreamStat,
canUserAccessDashboards,
canUserViewIntegrations,
};
};

View file

@ -14,7 +14,6 @@ import { DataStreamStat } from '../../common/data_streams_stats/data_stream_stat
import { tableSummaryAllText, tableSummaryOfText } from '../../common/translations';
import { getDatasetQualityTableColumns } from '../components/dataset_quality/table/columns';
import { useDatasetQualityContext } from '../components/dataset_quality/context';
import { FlyoutDataset } from '../state_machines/dataset_quality_controller';
import { useKibanaContextForPlugin } from '../utils';
import { filterInactiveDatasets, isActiveDataset } from '../utils/filter_inactive_datasets';
import { SortDirection } from '../../common/types';
@ -30,7 +29,10 @@ const sortingOverrides: Partial<{
export const useDatasetQualityTable = () => {
const {
services: { fieldFormats },
services: {
fieldFormats,
share: { url },
},
} = useKibanaContextForPlugin();
const { service } = useDatasetQualityContext();
@ -61,8 +63,6 @@ export const useDatasetQualityTable = () => {
} = useSelector(service, (state) => state.context.filters);
const showInactiveDatasets = inactive || !canUserMonitorDataset;
const flyout = useSelector(service, (state) => state.context.flyout);
const loading = useSelector(
service,
(state) =>
@ -89,33 +89,6 @@ export const useDatasetQualityTable = () => {
[service]
);
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 (!flyout?.insightsTimeRange) {
service.send({
type: 'OPEN_FLYOUT',
dataset: selectedDataset,
});
return;
}
service.send({
type: 'SELECT_NEW_DATASET',
dataset: selectedDataset,
});
},
[flyout?.dataset?.rawName, flyout?.insightsTimeRange, service]
);
const isActive = useCallback(
(lastActivity: number) => isActiveDataset({ lastActivity, timeRange }),
[timeRange]
@ -127,27 +100,25 @@ export const useDatasetQualityTable = () => {
fieldFormats,
canUserMonitorDataset,
canUserMonitorAnyDataStream,
selectedDataset: flyout?.dataset,
openFlyout,
loadingDataStreamStats,
loadingDegradedStats,
showFullDatasetNames,
isSizeStatsAvailable,
isActiveDataset: isActive,
timeRange,
urlService: url,
}),
[
fieldFormats,
canUserMonitorDataset,
canUserMonitorAnyDataStream,
flyout?.dataset,
openFlyout,
loadingDataStreamStats,
loadingDegradedStats,
showFullDatasetNames,
isSizeStatsAvailable,
isActive,
timeRange,
url,
]
);
@ -235,8 +206,6 @@ export const useDatasetQualityTable = () => {
columns,
loading,
resultsCount,
closeFlyout,
selectedDataset: flyout?.dataset,
showInactiveDatasets,
showFullDatasetNames,
canUserMonitorDataset,

View file

@ -0,0 +1,119 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useSelector } from '@xstate/react';
import { useCallback } from 'react';
import { getDateISORange } from '@kbn/timerange';
import { useDatasetQualityContext } from '../components/dataset_quality/context';
import { useDatasetQualityFilters } from './use_dataset_quality_filters';
import { DataStreamStat } from '../../common/data_streams_stats';
import { DatasetEbtProps, DatasetNavigatedEbtProps } from '../services/telemetry';
export function useDatasetTelemetry() {
const { service, telemetryClient } = useDatasetQualityContext();
// eslint-disable-next-line react-hooks/exhaustive-deps
const datasets = useSelector(service, (state) => state.context.datasets) ?? {};
const nonAggregatableDatasets = useSelector(
service,
(state) => state.context.nonAggregatableDatasets
);
const canUserViewIntegrations = useSelector(
service,
(state) => state.context.datasetUserPrivileges.canViewIntegrations
);
const sort = useSelector(service, (state) => state.context.table.sort);
const appliedFilters = useDatasetQualityFilters();
const trackDatasetNavigated = useCallback<(rawName: string, isIgnoredFilter: boolean) => void>(
(rawName: string, isIgnoredFilter: boolean) => {
const foundDataset = datasets.find((dataset) => dataset.rawName === rawName);
if (foundDataset) {
const ebtProps = getDatasetEbtProps(
foundDataset,
sort,
appliedFilters,
nonAggregatableDatasets,
isIgnoredFilter,
canUserViewIntegrations
);
telemetryClient.trackDatasetNavigated(ebtProps);
} else {
throw new Error(
`Cannot report dataset navigation telemetry for unknown dataset ${rawName}`
);
}
},
[
sort,
appliedFilters,
canUserViewIntegrations,
datasets,
nonAggregatableDatasets,
telemetryClient,
]
);
return { trackDatasetNavigated };
}
function getDatasetEbtProps(
dataset: DataStreamStat,
sort: { field: string; direction: 'asc' | 'desc' },
filters: ReturnType<typeof useDatasetQualityFilters>,
nonAggregatableDatasets: string[],
isIgnoredFilter: boolean,
canUserViewIntegrations: boolean
): DatasetNavigatedEbtProps {
const { startDate: from, endDate: to } = getDateISORange(filters.timeRange);
const datasetEbtProps: DatasetEbtProps = {
index_name: dataset.rawName,
data_stream: {
dataset: dataset.name,
namespace: dataset.namespace,
type: dataset.type,
},
data_stream_health: dataset.degradedDocs.quality,
data_stream_aggregatable: nonAggregatableDatasets.some(
(indexName) => indexName === dataset.rawName
),
from,
to,
degraded_percentage: dataset.degradedDocs.percentage,
integration: dataset.integration?.name,
privileges: {
can_monitor_data_stream: dataset.userPrivileges?.canMonitor ?? true,
can_view_integrations: canUserViewIntegrations,
},
};
const ebtFilters: DatasetNavigatedEbtProps['filters'] = {
is_degraded: isIgnoredFilter,
query_length: filters.selectedQuery?.length ?? 0,
integrations: {
total: filters.integrations.filter((item) => item.name !== 'none').length,
included: filters.integrations.filter((item) => item?.checked === 'on').length,
excluded: filters.integrations.filter((item) => item?.checked === 'off').length,
},
namespaces: {
total: filters.namespaces.length,
included: filters.namespaces.filter((item) => item?.checked === 'on').length,
excluded: filters.namespaces.filter((item) => item?.checked === 'off').length,
},
qualities: {
total: filters.qualities.length,
included: filters.qualities.filter((item) => item?.checked === 'on').length,
excluded: filters.qualities.filter((item) => item?.checked === 'off').length,
},
};
return {
...datasetEbtProps,
sort,
filters: ebtFilters,
};
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { useCallback, useState, useMemo, useEffect } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Action } from '@kbn/ui-actions-plugin/public';
import { fieldSupportsBreakdown } from '@kbn/unified-histogram-plugin/public';
import { i18n } from '@kbn/i18n';
@ -16,7 +16,9 @@ import { useCreateDataView } from './use_create_dataview';
import { useKibanaContextForPlugin } from '../utils';
import { useDatasetQualityDetailsState } from './use_dataset_quality_details_state';
import { getLensAttributes } from '../components/dataset_quality_details/overview/document_trends/degraded_docs/lens_attributes';
import { useDatasetQualityDetailsRedirectLink } from './use_dataset_quality_details_redirect_link';
import { useRedirectLink } from './use_redirect_link';
import { useDatasetDetailsTelemetry } from './use_dataset_details_telemetry';
import { useDatasetDetailsRedirectLinkTelemetry } from './use_redirect_link_telemetry';
const exploreDataInLogsExplorerText = i18n.translate(
'xpack.datasetQuality.details.chartExploreDataInLogsExplorerText',
@ -39,13 +41,27 @@ const openInLensText = i18n.translate('xpack.datasetQuality.details.chartOpenInL
const ACTION_EXPLORE_IN_LOGS_EXPLORER = 'ACTION_EXPLORE_IN_LOGS_EXPLORER';
const ACTION_OPEN_IN_LENS = 'ACTION_OPEN_IN_LENS';
export const useDegradedDocs = () => {
export const useDegradedDocsChart = () => {
const { euiTheme } = useEuiTheme();
const {
services: { lens },
} = useKibanaContextForPlugin();
const { service, dataStream, datasetDetails, timeRange, breakdownField, integrationDetails } =
useDatasetQualityDetailsState();
const {
service,
dataStream,
datasetDetails,
timeRange,
breakdownField,
integrationDetails,
isBreakdownFieldAsserted,
} = useDatasetQualityDetailsState();
const {
trackDatasetDetailsBreakdownFieldChanged,
trackDetailsNavigated,
navigationTargets,
navigationSources,
} = useDatasetDetailsTelemetry();
const [isChartLoading, setIsChartLoading] = useState<boolean | undefined>(undefined);
const [attributes, setAttributes] = useState<ReturnType<typeof getLensAttributes> | undefined>(
@ -75,6 +91,10 @@ export const useDegradedDocs = () => {
[service]
);
useEffect(() => {
if (isBreakdownFieldAsserted) trackDatasetDetailsBreakdownFieldChanged();
}, [trackDatasetDetailsBreakdownFieldChanged, isBreakdownFieldAsserted]);
useEffect(() => {
const dataStreamName = dataStream ?? DEFAULT_LOGS_DATA_VIEW;
const datasetTitle =
@ -98,13 +118,21 @@ export const useDegradedDocs = () => {
const openInLensCallback = useCallback(() => {
if (attributes) {
trackDetailsNavigated(navigationTargets.Lens, navigationSources.Chart);
lens.navigateToPrefilledEditor({
id: '',
timeRange,
attributes,
});
}
}, [attributes, lens, timeRange]);
}, [
attributes,
lens,
navigationSources.Chart,
navigationTargets.Lens,
timeRange,
trackDetailsNavigated,
]);
const getOpenInLensAction = useMemo(() => {
return {
@ -126,11 +154,17 @@ export const useDegradedDocs = () => {
};
}, [openInLensCallback]);
const redirectLinkProps = useDatasetQualityDetailsRedirectLink({
const { sendTelemetry } = useDatasetDetailsRedirectLinkTelemetry({
query: { language: 'kuery', query: '_ignored:*' },
navigationSource: navigationSources.Chart,
});
const redirectLinkProps = useRedirectLink({
dataStreamStat: datasetDetails,
query: { language: 'kuery', query: '_ignored:*' },
timeRangeConfig: timeRange,
breakdownField: breakdownDataViewField?.name,
sendTelemetry,
});
const getOpenInLogsExplorerAction = useMemo(() => {

View file

@ -1,224 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useCallback, useState, useMemo, useEffect } from 'react';
import { Action } from '@kbn/ui-actions-plugin/public';
import { fieldSupportsBreakdown } from '@kbn/unified-histogram-plugin/public';
import { useSelector } from '@xstate/react';
import { i18n } from '@kbn/i18n';
import { useEuiTheme } from '@elastic/eui';
import { type DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { useDatasetQualityContext } from '../components/dataset_quality/context';
import { DEFAULT_LOGS_DATA_VIEW } from '../../common/constants';
import { useCreateDataView } from './use_create_dataview';
import { useRedirectLink } from './use_redirect_link';
import { useDatasetQualityFlyout } from './use_dataset_quality_flyout';
import { useKibanaContextForPlugin } from '../utils';
import { useDatasetDetailsTelemetry } from './use_telemetry';
import { getLensAttributes } from '../components/dataset_quality_details/overview/document_trends/degraded_docs/lens_attributes';
const exploreDataInLogsExplorerText = i18n.translate(
'xpack.datasetQuality.flyoutChartExploreDataInLogsExplorerText',
{
defaultMessage: 'Explore data in Logs Explorer',
}
);
const exploreDataInDiscoverText = i18n.translate(
'xpack.datasetQuality.flyoutChartExploreDataInDiscoverText',
{
defaultMessage: 'Explore data in Discover',
}
);
const openInLensText = i18n.translate('xpack.datasetQuality.flyoutChartOpenInLensText', {
defaultMessage: 'Open in Lens',
});
const ACTION_EXPLORE_IN_LOGS_EXPLORER = 'ACTION_EXPLORE_IN_LOGS_EXPLORER';
const ACTION_OPEN_IN_LENS = 'ACTION_OPEN_IN_LENS';
interface DegradedDocsChartDeps {
dataStream?: string;
breakdownField?: string;
}
export const useDegradedDocsChart = ({ dataStream }: DegradedDocsChartDeps) => {
const { euiTheme } = useEuiTheme();
const {
services: { lens },
} = useKibanaContextForPlugin();
const { service } = useDatasetQualityContext();
const {
trackDatasetDetailsBreakdownFieldChanged,
trackDetailsNavigated,
navigationTargets,
navigationSources,
} = useDatasetDetailsTelemetry();
const { dataStreamStat, timeRange, breakdownField } = useDatasetQualityFlyout();
const isBreakdownFieldEcs = useSelector(
service,
(state) => state.context.flyout.isBreakdownFieldEcs
);
const isBreakdownFieldEcsAsserted = useSelector(service, (state) => {
return (
state.matches('flyout.initializing.assertBreakdownFieldIsEcs.done') &&
state.history?.matches('flyout.initializing.assertBreakdownFieldIsEcs.fetching') &&
isBreakdownFieldEcs !== null
);
});
const [isChartLoading, setIsChartLoading] = useState<boolean | undefined>(undefined);
const [attributes, setAttributes] = useState<ReturnType<typeof getLensAttributes> | undefined>(
undefined
);
const { dataView } = useCreateDataView({
indexPatternString: getDataViewIndexPattern(dataStream),
});
const breakdownDataViewField = useMemo(
() => getDataViewField(dataView, breakdownField),
[breakdownField, dataView]
);
const handleChartLoading = (isLoading: boolean) => {
setIsChartLoading(isLoading);
};
const handleBreakdownFieldChange = useCallback(
(field: DataViewField | undefined) => {
service.send({
type: 'BREAKDOWN_FIELD_CHANGE',
breakdownField: field?.name ?? null,
});
},
[service]
);
useEffect(() => {
if (isBreakdownFieldEcsAsserted) trackDatasetDetailsBreakdownFieldChanged();
}, [trackDatasetDetailsBreakdownFieldChanged, isBreakdownFieldEcsAsserted]);
useEffect(() => {
const dataStreamName = dataStream ?? DEFAULT_LOGS_DATA_VIEW;
const lensAttributes = getLensAttributes({
color: euiTheme.colors.danger,
dataStream: dataStreamName,
datasetTitle: dataStreamStat?.title ?? dataStreamName,
breakdownFieldName: breakdownDataViewField?.name,
});
setAttributes(lensAttributes);
}, [
breakdownDataViewField?.name,
euiTheme.colors.danger,
setAttributes,
dataStream,
dataStreamStat?.title,
]);
const openInLensCallback = useCallback(() => {
if (attributes) {
trackDetailsNavigated(navigationTargets.Lens, navigationSources.Chart);
lens.navigateToPrefilledEditor({
id: '',
timeRange,
attributes,
});
}
}, [attributes, trackDetailsNavigated, navigationTargets, navigationSources, lens, timeRange]);
const getOpenInLensAction = useMemo(() => {
return {
id: ACTION_OPEN_IN_LENS,
type: 'link',
order: 17,
getDisplayName(): string {
return openInLensText;
},
getIconType(): string {
return 'visArea';
},
async isCompatible(): Promise<boolean> {
return true;
},
async execute(): Promise<void> {
return openInLensCallback();
},
};
}, [openInLensCallback]);
const redirectLinkProps = useRedirectLink({
dataStreamStat: dataStreamStat!,
query: { language: 'kuery', query: '_ignored:*' },
timeRangeConfig: timeRange,
breakdownField: breakdownDataViewField?.name,
telemetry: {
page: 'details',
navigationSource: navigationSources.Chart,
},
});
const getOpenInLogsExplorerAction = useMemo(() => {
return {
id: ACTION_EXPLORE_IN_LOGS_EXPLORER,
type: 'link',
getDisplayName(): string {
return redirectLinkProps?.isLogsExplorerAvailable
? exploreDataInLogsExplorerText
: exploreDataInDiscoverText;
},
getHref: async () => {
return redirectLinkProps.linkProps.href;
},
getIconType(): string | undefined {
return 'visTable';
},
async isCompatible(): Promise<boolean> {
return true;
},
async execute(): Promise<void> {
return redirectLinkProps.navigate();
},
order: 18,
};
}, [redirectLinkProps]);
const extraActions: Action[] = [getOpenInLensAction, getOpenInLogsExplorerAction];
return {
attributes,
dataView,
breakdown: {
dataViewField: breakdownDataViewField,
fieldSupportsBreakdown: breakdownDataViewField
? fieldSupportsBreakdown(breakdownDataViewField)
: true,
onChange: handleBreakdownFieldChange,
},
extraActions,
isChartLoading,
onChartLoading: handleChartLoading,
setAttributes,
setIsChartLoading,
};
};
function getDataViewIndexPattern(dataStream: string | undefined) {
return dataStream ?? DEFAULT_LOGS_DATA_VIEW;
}
function getDataViewField(dataView: DataView | undefined, fieldName: string | undefined) {
return fieldName && dataView
? dataView.fields.find((field) => field.name === fieldName)
: undefined;
}

View file

@ -1,104 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getRouterLinkProps } from '@kbn/router-utils';
import { useMemo, useCallback } from 'react';
import useToggle from 'react-use/lib/useToggle';
import { MANAGEMENT_APP_LOCATOR } from '@kbn/deeplinks-management/constants';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { useKibanaContextForPlugin } from '../utils';
import { Dashboard } from '../../common/api_types';
import { useDatasetDetailsTelemetry } from './use_telemetry';
export const useFlyoutIntegrationActions = () => {
const {
services: {
application: { navigateToUrl },
http: { basePath },
share,
},
} = useKibanaContextForPlugin();
const { wrapLinkPropsForTelemetry, navigationSources, navigationTargets } =
useDatasetDetailsTelemetry();
const [isOpen, toggleIsOpen] = useToggle(false);
const dashboardLocator = useMemo(
() => share.url.locators.get(DASHBOARD_APP_LOCATOR),
[share.url.locators]
);
const indexManagementLocator = useMemo(
() => share.url.locators.get(MANAGEMENT_APP_LOCATOR),
[share.url.locators]
);
const handleCloseMenu = useCallback(() => {
toggleIsOpen();
}, [toggleIsOpen]);
const handleToggleMenu = useCallback(() => {
toggleIsOpen();
}, [toggleIsOpen]);
const getIntegrationOverviewLinkProps = useCallback(
(name: string, version: string) => {
const href = basePath.prepend(`/app/integrations/detail/${name}-${version}/overview`);
return wrapLinkPropsForTelemetry(
getRouterLinkProps({
href,
onClick: () => {
return navigateToUrl(href);
},
}),
navigationTargets.Integration,
navigationSources.ActionMenu
);
},
[basePath, navigateToUrl, navigationSources, navigationTargets, wrapLinkPropsForTelemetry]
);
const getIndexManagementLinkProps = useCallback(
(params: { sectionId: string; appId: string }) =>
wrapLinkPropsForTelemetry(
getRouterLinkProps({
href: indexManagementLocator?.getRedirectUrl(params),
onClick: () => {
return indexManagementLocator?.navigate(params);
},
}),
navigationTargets.IndexTemplate,
navigationSources.ActionMenu
),
[
indexManagementLocator,
navigationSources.ActionMenu,
navigationTargets.IndexTemplate,
wrapLinkPropsForTelemetry,
]
);
const getDashboardLinkProps = useCallback(
(dashboard: Dashboard) =>
wrapLinkPropsForTelemetry(
getRouterLinkProps({
href: dashboardLocator?.getRedirectUrl({ dashboardId: dashboard?.id } || ''),
onClick: () => {
return dashboardLocator?.navigate({ dashboardId: dashboard?.id } || '');
},
}),
navigationTargets.Dashboard,
navigationSources.ActionMenu
),
[dashboardLocator, navigationSources, navigationTargets, wrapLinkPropsForTelemetry]
);
return {
isOpen,
handleCloseMenu,
handleToggleMenu,
getIntegrationOverviewLinkProps,
getIndexManagementLinkProps,
getDashboardLinkProps,
};
};

View file

@ -12,6 +12,7 @@ import { MANAGEMENT_APP_LOCATOR } from '@kbn/deeplinks-management/constants';
import { DASHBOARD_APP_LOCATOR } from '@kbn/deeplinks-analytics';
import { useKibanaContextForPlugin } from '../utils';
import { Dashboard } from '../../common/api_types';
import { useDatasetDetailsTelemetry } from './use_dataset_details_telemetry';
export const useIntegrationActions = () => {
const {
@ -21,6 +22,8 @@ export const useIntegrationActions = () => {
share,
},
} = useKibanaContextForPlugin();
const { wrapLinkPropsForTelemetry, navigationSources, navigationTargets } =
useDatasetDetailsTelemetry();
const [isOpen, toggleIsOpen] = useToggle(false);
@ -43,34 +46,51 @@ export const useIntegrationActions = () => {
const getIntegrationOverviewLinkProps = useCallback(
(name: string, version: string) => {
const href = basePath.prepend(`/app/integrations/detail/${name}-${version}/overview`);
return getRouterLinkProps({
href,
onClick: () => {
return navigateToUrl(href);
},
});
return wrapLinkPropsForTelemetry(
getRouterLinkProps({
href,
onClick: () => {
return navigateToUrl(href);
},
}),
navigationTargets.Integration,
navigationSources.ActionMenu
);
},
[basePath, navigateToUrl]
[basePath, navigateToUrl, navigationSources, navigationTargets, wrapLinkPropsForTelemetry]
);
const getIndexManagementLinkProps = useCallback(
(params: { sectionId: string; appId: string }) =>
getRouterLinkProps({
href: indexManagementLocator?.getRedirectUrl(params),
onClick: () => {
return indexManagementLocator?.navigate(params);
},
}),
[indexManagementLocator]
wrapLinkPropsForTelemetry(
getRouterLinkProps({
href: indexManagementLocator?.getRedirectUrl(params),
onClick: () => {
return indexManagementLocator?.navigate(params);
},
}),
navigationTargets.IndexTemplate,
navigationSources.ActionMenu
),
[
indexManagementLocator,
navigationSources.ActionMenu,
navigationTargets.IndexTemplate,
wrapLinkPropsForTelemetry,
]
);
const getDashboardLinkProps = useCallback(
(dashboard: Dashboard) =>
getRouterLinkProps({
href: dashboardLocator?.getRedirectUrl({ dashboardId: dashboard?.id } || ''),
onClick: () => {
return dashboardLocator?.navigate({ dashboardId: dashboard?.id } || '');
},
}),
[dashboardLocator]
wrapLinkPropsForTelemetry(
getRouterLinkProps({
href: dashboardLocator?.getRedirectUrl({ dashboardId: dashboard?.id } || ''),
onClick: () => {
return dashboardLocator?.navigate({ dashboardId: dashboard?.id } || '');
},
}),
navigationTargets.Dashboard,
navigationSources.ActionMenu
),
[dashboardLocator, navigationSources, navigationTargets, wrapLinkPropsForTelemetry]
);
return {

View file

@ -18,20 +18,20 @@ import { LocatorPublic } from '@kbn/share-plugin/common';
import { LocatorClient } from '@kbn/shared-ux-prompt-no-data-views-types';
import { useKibanaContextForPlugin } from '../utils';
import { BasicDataStream, TimeRangeConfig } from '../../common/types';
import { useRedirectLinkTelemetry } from './use_telemetry';
import { SendTelemetryFn } from './use_redirect_link_telemetry';
export const useRedirectLink = <T extends BasicDataStream>({
dataStreamStat,
query,
timeRangeConfig,
breakdownField,
telemetry,
sendTelemetry,
}: {
dataStreamStat: T;
query?: Query | AggregateQuery;
timeRangeConfig: TimeRangeConfig;
breakdownField?: string;
telemetry?: Parameters<typeof useRedirectLinkTelemetry>[0]['telemetry'];
sendTelemetry: SendTelemetryFn;
}) => {
const {
services: { share },
@ -42,13 +42,6 @@ export const useRedirectLink = <T extends BasicDataStream>({
const logsExplorerLocator =
share.url.locators.get<SingleDatasetLocatorParams>(SINGLE_DATASET_LOCATOR_ID);
const { sendTelemetry } = useRedirectLinkTelemetry({
rawName: dataStreamStat.rawName,
isLogsExplorer: !!logsExplorerLocator,
telemetry,
query,
});
return useMemo<{
linkProps: RouterLinkProps;
navigate: () => void;

View file

@ -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 { useCallback } from 'react';
import { AggregateQuery, Query } from '@kbn/es-query';
import {
SINGLE_DATASET_LOCATOR_ID,
SingleDatasetLocatorParams,
} from '@kbn/deeplinks-observability';
import { NavigationSource } from '../services/telemetry';
import { useDatasetTelemetry } from './use_dataset_telemetry';
import { useDatasetDetailsTelemetry } from './use_dataset_details_telemetry';
import { useKibanaContextForPlugin } from '../utils';
export type SendTelemetryFn =
| ReturnType<typeof useDatasetRedirectLinkTelemetry>['sendTelemetry']
| ReturnType<typeof useDatasetDetailsRedirectLinkTelemetry>['sendTelemetry'];
export const useDatasetRedirectLinkTelemetry = ({
rawName,
query,
}: {
rawName: string;
query?: Query | AggregateQuery;
}) => {
const { trackDatasetNavigated } = useDatasetTelemetry();
const sendTelemetry = useCallback(() => {
const isIgnoredFilter = query ? JSON.stringify(query).includes('_ignored') : false;
trackDatasetNavigated(rawName, isIgnoredFilter);
}, [query, rawName, trackDatasetNavigated]);
return {
sendTelemetry,
};
};
export const useDatasetDetailsRedirectLinkTelemetry = ({
query,
navigationSource,
}: {
navigationSource: NavigationSource;
query?: Query | AggregateQuery;
}) => {
const {
services: { share },
} = useKibanaContextForPlugin();
const logsExplorerLocator =
share.url.locators.get<SingleDatasetLocatorParams>(SINGLE_DATASET_LOCATOR_ID);
const isLogsExplorer = !!logsExplorerLocator;
const { trackDetailsNavigated, navigationTargets } = useDatasetDetailsTelemetry();
const sendTelemetry = useCallback(() => {
const isIgnoredFilter = query ? JSON.stringify(query).includes('_ignored') : false;
const target = isLogsExplorer ? navigationTargets.LogsExplorer : navigationTargets.Discover;
trackDetailsNavigated(target, navigationSource, isIgnoredFilter);
}, [
query,
isLogsExplorer,
navigationTargets.LogsExplorer,
navigationTargets.Discover,
trackDetailsNavigated,
navigationSource,
]);
return {
sendTelemetry,
};
};

View file

@ -1,382 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { RouterLinkProps } from '@kbn/router-utils/src/get_router_link_props';
import { useCallback, useEffect, useMemo } from 'react';
import { useSelector } from '@xstate/react';
import { getDateISORange } from '@kbn/timerange';
import { AggregateQuery, Query } from '@kbn/es-query';
import { MASKED_FIELD_PLACEHOLDER, UNKOWN_FIELD_PLACEHOLDER } from '../../common/constants';
import { DataStreamStat } from '../../common/data_streams_stats';
import { DataStreamDetails } from '../../common/api_types';
import { mapPercentageToQuality } from '../../common/utils';
import {
NavigationTarget,
NavigationSource,
DatasetDetailsEbtProps,
DatasetNavigatedEbtProps,
DatasetEbtProps,
} from '../services/telemetry';
import { FlyoutDataset } from '../state_machines/dataset_quality_controller';
import { useDatasetQualityContext } from '../components/dataset_quality/context';
import { useDatasetQualityFilters } from './use_dataset_quality_filters';
import { TimeRangeConfig } from '../../common/types';
export const useRedirectLinkTelemetry = ({
rawName,
isLogsExplorer,
telemetry,
query,
}: {
rawName: string;
isLogsExplorer: boolean;
telemetry?: {
page: 'main' | 'details';
navigationSource: NavigationSource;
};
query?: Query | AggregateQuery;
}) => {
const { trackDatasetNavigated } = useDatasetTelemetry();
const { trackDetailsNavigated, navigationTargets } = useDatasetDetailsTelemetry();
const sendTelemetry = useCallback(() => {
if (telemetry) {
const isIgnoredFilter = query ? JSON.stringify(query).includes('_ignored') : false;
if (telemetry.page === 'main') {
trackDatasetNavigated(rawName, isIgnoredFilter);
} else {
trackDetailsNavigated(
isLogsExplorer ? navigationTargets.LogsExplorer : navigationTargets.Discover,
telemetry.navigationSource,
isIgnoredFilter
);
}
}
}, [
isLogsExplorer,
trackDetailsNavigated,
navigationTargets,
query,
rawName,
telemetry,
trackDatasetNavigated,
]);
const wrapLinkPropsForTelemetry = useCallback(
(props: RouterLinkProps) => {
return {
...props,
onClick: (event: Parameters<RouterLinkProps['onClick']>[0]) => {
sendTelemetry();
if (props.onClick) {
props.onClick(event);
}
},
};
},
[sendTelemetry]
);
return {
wrapLinkPropsForTelemetry,
sendTelemetry,
};
};
export const useDatasetTelemetry = () => {
const { service, telemetryClient } = useDatasetQualityContext();
// eslint-disable-next-line react-hooks/exhaustive-deps
const datasets = useSelector(service, (state) => state.context.datasets) ?? {};
const nonAggregatableDatasets = useSelector(
service,
(state) => state.context.nonAggregatableDatasets
);
const canUserViewIntegrations = useSelector(
service,
(state) => state.context.datasetUserPrivileges.canViewIntegrations
);
const sort = useSelector(service, (state) => state.context.table.sort);
const appliedFilters = useDatasetQualityFilters();
const trackDatasetNavigated = useCallback<(rawName: string, isIgnoredFilter: boolean) => void>(
(rawName: string, isIgnoredFilter: boolean) => {
const foundDataset = datasets.find((dataset) => dataset.rawName === rawName);
if (foundDataset) {
const ebtProps = getDatasetEbtProps(
foundDataset,
sort,
appliedFilters,
nonAggregatableDatasets,
isIgnoredFilter,
canUserViewIntegrations
);
telemetryClient.trackDatasetNavigated(ebtProps);
} else {
throw new Error(
`Cannot report dataset navigation telemetry for unknown dataset ${rawName}`
);
}
},
[
sort,
appliedFilters,
canUserViewIntegrations,
datasets,
nonAggregatableDatasets,
telemetryClient,
]
);
return { trackDatasetNavigated };
};
export const useDatasetDetailsTelemetry = () => {
const { service, telemetryClient } = useDatasetQualityContext();
const {
dataset: dataStreamStat,
datasetDetails: dataStreamDetails,
insightsTimeRange,
breakdownField,
isNonAggregatable,
isBreakdownFieldEcs,
} = useSelector(service, (state) => state.context.flyout) ?? {};
const loadingState = useSelector(service, (state) => ({
dataStreamDetailsLoading:
state.matches('flyout.initializing.dataStreamDetails.fetching') ||
state.matches('flyout.initializing.assertBreakdownFieldIsEcs.fetching'),
}));
const canUserAccessDashboards = useSelector(
service,
(state) => !state.matches('flyout.initializing.integrationDashboards.unauthorized')
);
const canUserViewIntegrations = useSelector(
service,
(state) => state.context.datasetUserPrivileges.canViewIntegrations
);
const ebtProps = useMemo<DatasetDetailsEbtProps | undefined>(() => {
if (
dataStreamDetails &&
insightsTimeRange &&
dataStreamStat &&
!loadingState.dataStreamDetailsLoading
) {
return getDatasetDetailsEbtProps(
insightsTimeRange,
dataStreamStat,
dataStreamDetails,
isNonAggregatable ?? false,
canUserViewIntegrations,
canUserAccessDashboards,
isBreakdownFieldEcs,
breakdownField
);
}
return undefined;
}, [
insightsTimeRange,
dataStreamStat,
dataStreamDetails,
loadingState.dataStreamDetailsLoading,
isNonAggregatable,
canUserViewIntegrations,
canUserAccessDashboards,
isBreakdownFieldEcs,
breakdownField,
]);
const startTracking = useCallback(() => {
telemetryClient.startDatasetDetailsTracking();
}, [telemetryClient]);
// Report opening dataset details
useEffect(() => {
const datasetDetailsTrackingState = telemetryClient.getDatasetDetailsTrackingState();
if (datasetDetailsTrackingState === 'started' && ebtProps) {
telemetryClient.trackDatasetDetailsOpened(ebtProps);
}
}, [ebtProps, telemetryClient]);
const trackDetailsNavigated = useCallback(
(target: NavigationTarget, source: NavigationSource, isDegraded = false) => {
const datasetDetailsTrackingState = telemetryClient.getDatasetDetailsTrackingState();
if (
(datasetDetailsTrackingState === 'opened' || datasetDetailsTrackingState === 'navigated') &&
ebtProps
) {
telemetryClient.trackDatasetDetailsNavigated({
...ebtProps,
filters: {
is_degraded: isDegraded,
},
target,
source,
});
} else {
throw new Error(
'Cannot report dataset details navigation telemetry without required data and state'
);
}
},
[ebtProps, telemetryClient]
);
const trackDatasetDetailsBreakdownFieldChanged = useCallback(() => {
const datasetDetailsTrackingState = telemetryClient.getDatasetDetailsTrackingState();
if (
(datasetDetailsTrackingState === 'opened' || datasetDetailsTrackingState === 'navigated') &&
ebtProps
) {
telemetryClient.trackDatasetDetailsBreakdownFieldChanged({
...ebtProps,
breakdown_field: ebtProps.breakdown_field,
});
}
}, [ebtProps, telemetryClient]);
const wrapLinkPropsForTelemetry = useCallback(
(
props: RouterLinkProps,
target: NavigationTarget,
source: NavigationSource,
isDegraded = false
) => {
return {
...props,
onClick: (event: Parameters<RouterLinkProps['onClick']>[0]) => {
trackDetailsNavigated(target, source, isDegraded);
if (props.onClick) {
props.onClick(event);
}
},
};
},
[trackDetailsNavigated]
);
return {
startTracking,
trackDetailsNavigated,
wrapLinkPropsForTelemetry,
navigationTargets: NavigationTarget,
navigationSources: NavigationSource,
trackDatasetDetailsBreakdownFieldChanged,
};
};
function getDatasetEbtProps(
dataset: DataStreamStat,
sort: { field: string; direction: 'asc' | 'desc' },
filters: ReturnType<typeof useDatasetQualityFilters>,
nonAggregatableDatasets: string[],
isIgnoredFilter: boolean,
canUserViewIntegrations: boolean
): DatasetNavigatedEbtProps {
const { startDate: from, endDate: to } = getDateISORange(filters.timeRange);
const datasetEbtProps: DatasetEbtProps = {
index_name: dataset.rawName,
data_stream: {
dataset: dataset.name,
namespace: dataset.namespace,
type: dataset.type,
},
data_stream_health: dataset.degradedDocs.quality,
data_stream_aggregatable: nonAggregatableDatasets.some(
(indexName) => indexName === dataset.rawName
),
from,
to,
degraded_percentage: dataset.degradedDocs.percentage,
integration: dataset.integration?.name,
privileges: {
can_monitor_data_stream: dataset.userPrivileges?.canMonitor ?? true,
can_view_integrations: canUserViewIntegrations,
},
};
const ebtFilters: DatasetNavigatedEbtProps['filters'] = {
is_degraded: isIgnoredFilter,
query_length: filters.selectedQuery?.length ?? 0,
integrations: {
total: filters.integrations.filter((item) => item.name !== 'none').length,
included: filters.integrations.filter((item) => item?.checked === 'on').length,
excluded: filters.integrations.filter((item) => item?.checked === 'off').length,
},
namespaces: {
total: filters.namespaces.length,
included: filters.namespaces.filter((item) => item?.checked === 'on').length,
excluded: filters.namespaces.filter((item) => item?.checked === 'off').length,
},
qualities: {
total: filters.qualities.length,
included: filters.qualities.filter((item) => item?.checked === 'on').length,
excluded: filters.qualities.filter((item) => item?.checked === 'off').length,
},
};
return {
...datasetEbtProps,
sort,
filters: ebtFilters,
};
}
function getDatasetDetailsEbtProps(
insightsTimeRange: TimeRangeConfig,
flyoutDataset: FlyoutDataset,
details: DataStreamDetails,
isNonAggregatable: boolean,
canUserViewIntegrations: boolean,
canUserAccessDashboards: boolean,
isBreakdownFieldEcs: boolean | null,
breakdownField?: string
): DatasetDetailsEbtProps {
const indexName = flyoutDataset.rawName;
const dataStream = {
dataset: flyoutDataset.name,
namespace: flyoutDataset.namespace,
type: flyoutDataset.type,
};
const degradedDocs = details?.degradedDocsCount ?? 0;
const totalDocs = details?.docsCount ?? 0;
const degradedPercentage =
totalDocs > 0 ? Number(((degradedDocs / totalDocs) * 100).toFixed(2)) : 0;
const health = mapPercentageToQuality(degradedPercentage);
const { startDate: from, endDate: to } = getDateISORange(insightsTimeRange);
return {
index_name: indexName,
data_stream: dataStream,
privileges: {
can_monitor_data_stream: true,
can_view_integrations: canUserViewIntegrations,
can_view_dashboards: canUserAccessDashboards,
},
data_stream_aggregatable: !isNonAggregatable,
data_stream_health: health,
from,
to,
degraded_percentage: degradedPercentage,
integration: flyoutDataset.integration?.name,
breakdown_field: breakdownField
? isBreakdownFieldEcs === null
? UNKOWN_FIELD_PLACEHOLDER
: getMaskedBreakdownField(breakdownField, isBreakdownFieldEcs)
: breakdownField,
};
}
function getMaskedBreakdownField(breakdownField: string, isBreakdownFieldEcs: boolean) {
return isBreakdownFieldEcs ? breakdownField : MASKED_FIELD_PLACEHOLDER;
}

View file

@ -52,9 +52,7 @@ export class DatasetQualityPlugin
const createDatasetQualityController = createDatasetQualityControllerLazyFactory({
core,
plugins,
dataStreamStatsClient,
dataStreamDetailsClient,
});
const DatasetQualityDetails = createDatasetQualityDetails({

View file

@ -34,6 +34,7 @@ export enum NavigationSource {
Footer = 'footer',
Summary = 'summary',
Chart = 'chart',
Trend = 'trend',
Table = 'table',
ActionMenu = 'action_menu',
}

View file

@ -7,8 +7,6 @@
import {
DEFAULT_DATASET_TYPE,
DEFAULT_DEGRADED_FIELD_SORT_DIRECTION,
DEFAULT_DEGRADED_FIELD_SORT_FIELD,
DEFAULT_SORT_DIRECTION,
DEFAULT_SORT_FIELD,
} from '../../../../common/constants';
@ -47,19 +45,6 @@ export const DEFAULT_CONTEXT: DefaultDatasetQualityControllerState = {
namespaces: [],
qualities: [],
},
flyout: {
degradedFields: {
table: {
page: 0,
rowsPerPage: 10,
sort: {
field: DEFAULT_DEGRADED_FIELD_SORT_FIELD,
direction: DEFAULT_DEGRADED_FIELD_SORT_DIRECTION,
},
},
},
isBreakdownFieldEcs: null,
},
datasets: [],
isSizeStatsAvailable: true,
nonAggregatableDatasets: [],

View file

@ -17,15 +17,6 @@ export const fetchDatasetStatsFailedNotifier = (toasts: IToasts, error: Error) =
});
};
export const fetchDatasetDetailsFailedNotifier = (toasts: IToasts, error: Error) => {
toasts.addDanger({
title: i18n.translate('xpack.datasetQuality.fetchDatasetDetailsFailed', {
defaultMessage: "We couldn't get your data set details.",
}),
text: error.message,
});
};
export const fetchDegradedStatsFailedNotifier = (toasts: IToasts, error: Error) => {
toasts.addDanger({
title: i18n.translate('xpack.datasetQuality.fetchDegradedStatsFailed', {
@ -35,15 +26,6 @@ export const fetchDegradedStatsFailedNotifier = (toasts: IToasts, error: Error)
});
};
export const fetchNonAggregatableDatasetsFailedNotifier = (toasts: IToasts, error: Error) => {
toasts.addDanger({
title: i18n.translate('xpack.datasetQuality.fetchNonAggregatableDatasetsFailed', {
defaultMessage: "We couldn't get non aggregatable datasets information.",
}),
text: error.message,
});
};
export const fetchIntegrationsFailedNotifier = (toasts: IToasts, error: Error) => {
toasts.addDanger({
title: i18n.translate('xpack.datasetQuality.fetchIntegrationsFailed', {
@ -52,19 +34,3 @@ export const fetchIntegrationsFailedNotifier = (toasts: IToasts, error: Error) =
text: error.message,
});
};
export const noDatasetSelected = i18n.translate(
'xpack.datasetQuality.fetchDatasetDetailsFailed.noDatasetSelected',
{
defaultMessage: 'No data set have been selected',
}
);
export const assertBreakdownFieldEcsFailedNotifier = (toasts: IToasts, error: Error) => {
toasts.addDanger({
title: i18n.translate('xpack.datasetQuality.assertBreakdownFieldEcsFailed', {
defaultMessage: "We couldn't retrieve breakdown field metadata.",
}),
text: error.message,
});
};

View file

@ -8,36 +8,22 @@
import { IToasts } from '@kbn/core/public';
import { getDateISORange } from '@kbn/timerange';
import { assign, createMachine, DoneInvokeEvent, InterpreterFrom } from 'xstate';
import { DatasetQualityStartDeps } from '../../../types';
import {
Dashboard,
DataStreamStat,
DegradedFieldResponse,
NonAggregatableDatasets,
} from '../../../../common/api_types';
import { DataStreamStat, NonAggregatableDatasets } from '../../../../common/api_types';
import { Integration } from '../../../../common/data_streams_stats/integration';
import { IDataStreamDetailsClient } from '../../../services/data_stream_details';
import {
DataStreamSettings,
DataStreamDetails,
GetDataStreamsStatsQuery,
GetIntegrationsParams,
GetNonAggregatableDataStreamsParams,
DataStreamStatServiceResponse,
} from '../../../../common/data_streams_stats';
import { DegradedDocsStat } from '../../../../common/data_streams_stats/malformed_docs_stat';
import { DataStreamType } from '../../../../common/types';
import { dataStreamPartsToIndexName } from '../../../../common/utils';
import { IDataStreamsStatsClient } from '../../../services/data_streams_stats';
import { generateDatasets } from '../../../utils';
import { DEFAULT_CONTEXT } from './defaults';
import {
fetchDatasetDetailsFailedNotifier,
fetchDatasetStatsFailedNotifier,
fetchDegradedStatsFailedNotifier,
fetchIntegrationsFailedNotifier,
noDatasetSelected,
assertBreakdownFieldEcsFailedNotifier,
} from './notifications';
import { fetchNonAggregatableDatasetsFailedNotifier } from '../../common/notifications';
import {
@ -45,13 +31,7 @@ import {
DatasetQualityControllerEvent,
DatasetQualityControllerTypeState,
DefaultDatasetQualityControllerState,
FlyoutDataset,
} from './types';
import {
fetchDataStreamSettingsFailedNotifier,
fetchIntegrationDashboardsFailedNotifier,
fetchDataStreamIntegrationFailedNotifier,
} from '../../dataset_quality_details_controller/notifications';
export const createPureDatasetQualityControllerStateMachine = (
initialContext: DatasetQualityControllerContext
@ -227,231 +207,6 @@ export const createPureDatasetQualityControllerStateMachine = (
},
},
},
flyout: {
initial: 'closed',
states: {
initializing: {
type: 'parallel',
states: {
nonAggregatableDataset: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'loadDatasetIsNonAggregatable',
onDone: {
target: 'done',
actions: ['storeDatasetIsNonAggregatable'],
},
onError: {
target: 'done',
actions: ['notifyFetchNonAggregatableDatasetsFailed'],
},
},
},
done: {
on: {
UPDATE_INSIGHTS_TIME_RANGE: {
target: 'fetching',
actions: ['storeFlyoutOptions'],
},
SELECT_DATASET: {
target: 'fetching',
actions: ['storeFlyoutOptions'],
},
},
},
},
},
dataStreamSettings: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'loadDataStreamSettings',
onDone: {
target: 'initializeIntegrations',
actions: ['storeDataStreamSettings'],
},
onError: {
target: 'done',
actions: ['notifyFetchDataStreamSettingsFailed'],
},
},
},
initializeIntegrations: {
type: 'parallel',
states: {
integrationDetails: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'loadDataStreamIntegration',
onDone: {
target: 'done',
actions: ['storeDataStreamIntegration'],
},
onError: {
target: 'done',
actions: ['notifyFetchDatasetIntegrationsFailed'],
},
},
},
done: {
type: 'final',
},
},
},
integrationDashboards: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'loadIntegrationDashboards',
onDone: {
target: 'done',
actions: ['storeIntegrationDashboards'],
},
onError: [
{
target: 'unauthorized',
cond: 'checkIfActionForbidden',
},
{
target: 'done',
actions: ['notifyFetchIntegrationDashboardsFailed'],
},
],
},
},
done: {
type: 'final',
},
unauthorized: {
type: 'final',
},
},
},
},
},
done: {
type: 'final',
},
},
},
dataStreamDetails: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'loadDataStreamDetails',
onDone: {
target: 'done',
actions: ['storeDatasetDetails'],
},
onError: {
target: 'done',
actions: ['notifyFetchDatasetDetailsFailed'],
},
},
},
done: {
on: {
UPDATE_INSIGHTS_TIME_RANGE: {
target: 'fetching',
actions: ['storeFlyoutOptions'],
},
BREAKDOWN_FIELD_CHANGE: {
target:
'#DatasetQualityController.flyout.initializing.assertBreakdownFieldIsEcs.fetching',
actions: ['storeFlyoutOptions'],
},
},
},
},
},
dataStreamDegradedFields: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'loadDegradedFieldsPerDataStream',
onDone: {
target: 'done',
actions: ['storeDegradedFields'],
},
onError: {
target: 'done',
},
},
},
done: {
on: {
UPDATE_INSIGHTS_TIME_RANGE: {
target: 'fetching',
actions: ['resetDegradedFieldPage'],
},
UPDATE_DEGRADED_FIELDS_TABLE_CRITERIA: {
target: 'done',
actions: ['storeDegradedFieldTableOptions'],
},
},
},
},
},
assertBreakdownFieldIsEcs: {
initial: 'fetching',
states: {
fetching: {
invoke: {
src: 'assertBreakdownFieldIsEcs',
onDone: {
target: 'done',
actions: ['storeBreakdownFieldIsEcs'],
},
onError: {
target: 'done',
actions: ['notifyAssertBreakdownFieldEcsFailed'],
},
},
},
done: {},
},
},
},
onDone: {
target: '#DatasetQualityController.flyout.loaded',
},
},
loaded: {
on: {
CLOSE_FLYOUT: {
target: 'closed',
actions: ['resetFlyoutOptions'],
},
},
},
closed: {
on: {
OPEN_FLYOUT: {
target: '#DatasetQualityController.flyout.initializing',
actions: ['storeFlyoutOptions'],
},
},
},
},
on: {
SELECT_NEW_DATASET: {
target: '#DatasetQualityController.flyout.initializing',
actions: ['storeFlyoutOptions'],
},
CLOSE_FLYOUT: {
target: '#DatasetQualityController.flyout.closed',
actions: ['resetFlyoutOptions'],
},
},
},
},
},
{
@ -463,38 +218,12 @@ export const createPureDatasetQualityControllerStateMachine = (
}
: {};
}),
storeDegradedFieldTableOptions: assign((context, event) => {
return 'degraded_field_criteria' in event
? {
flyout: {
...context.flyout,
degradedFields: {
...context.flyout.degradedFields,
table: event.degraded_field_criteria,
},
},
}
: {};
}),
resetPage: assign((context, _event) => ({
table: {
...context.table,
page: 0,
},
})),
resetDegradedFieldPage: assign((context, _event) => ({
flyout: {
...context.flyout,
degradedFields: {
...context.flyout.degradedFields,
table: {
...context.flyout.degradedFields.table,
page: 0,
rowsPerPage: 10,
},
},
},
})),
storeInactiveDatasetsVisibility: assign((context, _event) => {
return {
filters: {
@ -561,37 +290,6 @@ export const createPureDatasetQualityControllerStateMachine = (
}
: {};
}),
storeFlyoutOptions: assign((context, event) => {
const insightsTimeRange =
'timeRange' in event
? event.timeRange
: context.flyout?.insightsTimeRange ?? context.filters?.timeRange;
const dataset =
'dataset' in event ? (event.dataset as FlyoutDataset) : context.flyout?.dataset;
const breakdownField =
'breakdownField' in event
? event.breakdownField ?? undefined
: context.flyout?.breakdownField;
return {
flyout: {
...context.flyout,
dataset,
insightsTimeRange,
breakdownField,
},
};
}),
storeBreakdownFieldIsEcs: assign((context, event: DoneInvokeEvent<boolean | null>) => {
return {
flyout: {
...context.flyout,
isBreakdownFieldEcs:
'data' in event && typeof event.data === 'boolean' ? event.data : null,
},
};
}),
resetFlyoutOptions: assign((_context, _event) => ({ flyout: DEFAULT_CONTEXT.flyout })),
storeDataStreamStats: assign(
(_context, event: DoneInvokeEvent<DataStreamStatServiceResponse>) => {
if ('data' in event && 'dataStreamsStats' in event.data) {
@ -618,19 +316,6 @@ export const createPureDatasetQualityControllerStateMachine = (
}
: {};
}),
storeDegradedFields: assign((context, event: DoneInvokeEvent<DegradedFieldResponse>) => {
return 'data' in event
? {
flyout: {
...context.flyout,
degradedFields: {
...context.flyout.degradedFields,
data: event.data.degradedFields,
},
},
}
: {};
}),
storeNonAggregatableDatasets: assign(
(
_context: DefaultDatasetQualityControllerState,
@ -643,41 +328,6 @@ export const createPureDatasetQualityControllerStateMachine = (
: {};
}
),
storeDataStreamSettings: assign((context, event) => {
return 'data' in event
? {
flyout: {
...context.flyout,
dataStreamSettings: (event.data ?? {}) as DataStreamSettings,
},
}
: {};
}),
storeDatasetDetails: assign((context, event) => {
return 'data' in event
? {
flyout: {
...context.flyout,
datasetDetails: event.data as DataStreamDetails,
},
}
: {};
}),
storeDatasetIsNonAggregatable: assign(
(
context: DefaultDatasetQualityControllerState,
event: DoneInvokeEvent<NonAggregatableDatasets>
) => {
return 'data' in event
? {
flyout: {
...context.flyout,
isNonAggregatable: !event.data.aggregatable,
},
}
: {};
}
),
storeIntegrations: assign((_context, event) => {
return 'data' in event
? {
@ -690,32 +340,6 @@ export const createPureDatasetQualityControllerStateMachine = (
integrations: [],
};
}),
storeDataStreamIntegration: assign((context, event: DoneInvokeEvent<Integration>) => {
return 'data' in event
? {
flyout: {
...context.flyout,
integration: {
...context.flyout.integration,
integrationDetails: event.data,
},
},
}
: {};
}),
storeIntegrationDashboards: assign((context, event: DoneInvokeEvent<Dashboard[]>) => {
return 'data' in event
? {
flyout: {
...context.flyout,
integration: {
...context.flyout.integration,
dashboards: event.data,
},
},
}
: {};
}),
storeDatasets: assign((context, _event) => {
return context.integrations && (context.dataStreamStats || context.degradedDocStats)
? {
@ -743,18 +367,14 @@ export const createPureDatasetQualityControllerStateMachine = (
export interface DatasetQualityControllerStateMachineDependencies {
initialContext?: DatasetQualityControllerContext;
plugins: DatasetQualityStartDeps;
toasts: IToasts;
dataStreamStatsClient: IDataStreamsStatsClient;
dataStreamDetailsClient: IDataStreamDetailsClient;
}
export const createDatasetQualityControllerStateMachine = ({
initialContext = DEFAULT_CONTEXT,
plugins,
toasts,
dataStreamStatsClient,
dataStreamDetailsClient,
}: DatasetQualityControllerStateMachineDependencies) =>
createPureDatasetQualityControllerStateMachine(initialContext).withConfig({
actions: {
@ -764,20 +384,8 @@ export const createDatasetQualityControllerStateMachine = ({
fetchDegradedStatsFailedNotifier(toasts, event.data),
notifyFetchNonAggregatableDatasetsFailed: (_context, event: DoneInvokeEvent<Error>) =>
fetchNonAggregatableDatasetsFailedNotifier(toasts, event.data),
notifyFetchDataStreamSettingsFailed: (_context, event: DoneInvokeEvent<Error>) =>
fetchDataStreamSettingsFailedNotifier(toasts, event.data),
notifyFetchDatasetDetailsFailed: (_context, event: DoneInvokeEvent<Error>) =>
fetchDatasetDetailsFailedNotifier(toasts, event.data),
notifyFetchIntegrationDashboardsFailed: (_context, event: DoneInvokeEvent<Error>) =>
fetchIntegrationDashboardsFailedNotifier(toasts, event.data),
notifyFetchIntegrationsFailed: (_context, event: DoneInvokeEvent<Error>) =>
fetchIntegrationsFailedNotifier(toasts, event.data),
notifyFetchDatasetIntegrationsFailed: (context, event: DoneInvokeEvent<Error>) => {
const integrationName = context.flyout.dataStreamSettings?.integration;
return fetchDataStreamIntegrationFailedNotifier(toasts, event.data, integrationName);
},
notifyAssertBreakdownFieldEcsFailed: (_context, event: DoneInvokeEvent<Error>) =>
assertBreakdownFieldEcsFailedNotifier(toasts, event.data),
},
services: {
loadDataStreamStats: (context) =>
@ -795,27 +403,6 @@ export const createDatasetQualityControllerStateMachine = ({
end,
});
},
loadDegradedFieldsPerDataStream: (context) => {
if (!context.flyout.dataset || !context.flyout.insightsTimeRange) {
return Promise.resolve({});
}
const { startDate: start, endDate: end } = getDateISORange(
context.flyout.insightsTimeRange
);
const { type, name: dataset, namespace } = context.flyout.dataset;
return dataStreamDetailsClient.getDataStreamDegradedFields({
dataStream: dataStreamPartsToIndexName({
type: type as DataStreamType,
dataset,
namespace,
}),
start,
end,
});
},
loadIntegrations: (context) => {
return dataStreamStatsClient.getIntegrations({
type: context.type as GetIntegrationsParams['query']['type'],
@ -830,108 +417,6 @@ export const createDatasetQualityControllerStateMachine = ({
end,
});
},
loadDataStreamSettings: (context) => {
if (!context.flyout.dataset) {
fetchDataStreamSettingsFailedNotifier(toasts, new Error(noDatasetSelected));
return Promise.resolve({});
}
const { type, name: dataset, namespace } = context.flyout.dataset;
return dataStreamDetailsClient.getDataStreamSettings({
dataStream: dataStreamPartsToIndexName({
type: type as DataStreamType,
dataset,
namespace,
}),
});
},
loadDataStreamIntegration: (context) => {
if (context.flyout.dataStreamSettings?.integration && context.flyout.dataset) {
const { type } = context.flyout.dataset;
return dataStreamDetailsClient.getDataStreamIntegration({
type: type as DataStreamType,
integrationName: context.flyout.dataStreamSettings.integration,
});
}
return Promise.resolve();
},
loadDataStreamDetails: (context) => {
if (!context.flyout.dataset || !context.flyout.insightsTimeRange) {
fetchDatasetDetailsFailedNotifier(toasts, new Error(noDatasetSelected));
return Promise.resolve({});
}
const { type, name: dataset, namespace } = context.flyout.dataset;
const { startDate: start, endDate: end } = getDateISORange(
context.flyout.insightsTimeRange
);
return dataStreamDetailsClient.getDataStreamDetails({
dataStream: dataStreamPartsToIndexName({
type: type as DataStreamType,
dataset,
namespace,
}),
start,
end,
});
},
loadIntegrationDashboards: (context) => {
if (context.flyout.dataStreamSettings?.integration) {
return dataStreamDetailsClient.getIntegrationDashboards({
integration: context.flyout.dataStreamSettings.integration,
});
}
return Promise.resolve();
},
loadDatasetIsNonAggregatable: async (context) => {
if (!context.flyout.dataset || !context.flyout.insightsTimeRange) {
fetchDatasetDetailsFailedNotifier(toasts, new Error(noDatasetSelected));
return Promise.resolve({});
}
const { type, name: dataset, namespace } = context.flyout.dataset;
const { startDate: start, endDate: end } = getDateISORange(
context.flyout.insightsTimeRange
);
return dataStreamStatsClient.getNonAggregatableDatasets({
type: context.type as GetNonAggregatableDataStreamsParams['type'],
start,
end,
dataStream: dataStreamPartsToIndexName({
type: type as DataStreamType,
dataset,
namespace,
}),
});
},
assertBreakdownFieldIsEcs: async (context) => {
if (context.flyout.breakdownField) {
const allowedFieldSources = ['ecs', 'metadata'];
// This timeout is to avoid a runtime error that randomly happens on breakdown field change
// TypeError: Cannot read properties of undefined (reading 'timeFieldName')
await new Promise((res) => setTimeout(res, 300));
const client = await plugins.fieldsMetadata.getClient();
const { fields } = await client.find({
attributes: ['source'],
fieldNames: [context.flyout.breakdownField],
});
const breakdownFieldSource = fields[context.flyout.breakdownField]?.source;
return !!(breakdownFieldSource && allowedFieldSources.includes(breakdownFieldSource));
}
return null;
},
},
});

View file

@ -7,35 +7,18 @@
import { DoneInvokeEvent } from 'xstate';
import { QualityIndicators, TableCriteria, TimeRangeConfig } from '../../../../common/types';
import {
Dashboard,
DatasetUserPrivileges,
NonAggregatableDatasets,
} from '../../../../common/api_types';
import { DatasetUserPrivileges, NonAggregatableDatasets } from '../../../../common/api_types';
import { Integration } from '../../../../common/data_streams_stats/integration';
import { DatasetTableSortField, DegradedFieldSortField } from '../../../hooks';
import { DatasetTableSortField } from '../../../hooks';
import { DegradedDocsStat } from '../../../../common/data_streams_stats/malformed_docs_stat';
import {
DataStreamDegradedDocsStatServiceResponse,
DataStreamSettings,
DataStreamDetails,
DataStreamStatServiceResponse,
DataStreamStat,
DataStreamStatType,
DegradedField,
DegradedFieldResponse,
} from '../../../../common/data_streams_stats';
export type FlyoutDataset = Omit<
DataStreamStat,
'type' | 'size' | 'sizeBytes' | 'lastActivity' | 'degradedDocs'
> & { type: string };
export interface DegradedFields {
table: TableCriteria<DegradedFieldSortField>;
data?: DegradedField[];
}
interface FiltersCriteria {
inactive: boolean;
fullNames: boolean;
@ -46,29 +29,10 @@ interface FiltersCriteria {
query?: string;
}
export interface DataStreamIntegrations {
integrationDetails?: Integration;
dashboards?: Dashboard[];
}
export interface WithTableOptions {
table: TableCriteria<DatasetTableSortField>;
}
export interface WithFlyoutOptions {
flyout: {
dataset?: FlyoutDataset;
dataStreamSettings?: DataStreamSettings;
datasetDetails?: DataStreamDetails;
insightsTimeRange?: TimeRangeConfig;
breakdownField?: string;
degradedFields: DegradedFields;
isNonAggregatable?: boolean;
integration?: DataStreamIntegrations;
isBreakdownFieldEcs: boolean | null;
};
}
export interface WithFilters {
filters: FiltersCriteria;
}
@ -98,14 +62,12 @@ export interface WithIntegrations {
export type DefaultDatasetQualityControllerState = { type: string } & WithTableOptions &
WithDataStreamStats &
Partial<WithDegradedDocs> &
WithFlyoutOptions &
WithDatasets &
WithFilters &
WithNonAggregatableDatasets &
Partial<WithIntegrations>;
type DefaultDatasetQualityStateContext = DefaultDatasetQualityControllerState &
Partial<WithFlyoutOptions>;
type DefaultDatasetQualityStateContext = DefaultDatasetQualityControllerState;
export type DatasetQualityControllerTypeState =
| {
@ -131,48 +93,6 @@ export type DatasetQualityControllerTypeState =
| {
value: 'nonAggregatableDatasets.fetching';
context: DefaultDatasetQualityStateContext;
}
| {
value: 'flyout.initializing.dataStreamSettings.fetching';
context: DefaultDatasetQualityStateContext;
}
| {
value: 'flyout.initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.fetching';
context: DefaultDatasetQualityStateContext;
}
| {
value: 'flyout.initializing.dataStreamSettings.initializeIntegrations.integrationDashboards.unauthorized';
context: DefaultDatasetQualityStateContext;
}
| {
value: 'flyout.initializing.dataStreamSettings.initializeIntegrations.integrationDetails.done';
context: DefaultDatasetQualityStateContext;
}
| {
value: 'flyout.initializing.dataStreamDetails.fetching';
context: DefaultDatasetQualityStateContext;
}
| {
value: 'flyout.initializing.dataStreamDetails.done';
context: DefaultDatasetQualityStateContext;
}
| {
value: 'flyout.initializing.assertBreakdownFieldIsEcs.fetching';
context: DefaultDatasetQualityStateContext;
}
| {
value: 'flyout.initializing.assertBreakdownFieldIsEcs.done';
context: DefaultDatasetQualityStateContext;
}
| {
value: 'flyout.initializing.dataStreamDegradedFields.fetching';
context: DefaultDatasetQualityStateContext;
}
| {
value:
| 'flyout.initializing.integrationDashboards.fetching'
| 'flyout.initializing.integrationDashboards.unauthorized';
context: DefaultDatasetQualityStateContext;
};
export type DatasetQualityControllerContext = DatasetQualityControllerTypeState['context'];
@ -182,29 +102,10 @@ export type DatasetQualityControllerEvent =
type: 'UPDATE_TABLE_CRITERIA';
dataset_criteria: TableCriteria<DatasetTableSortField>;
}
| {
type: 'UPDATE_DEGRADED_FIELDS_TABLE_CRITERIA';
degraded_field_criteria: TableCriteria<DegradedFieldSortField>;
}
| {
type: 'OPEN_FLYOUT';
dataset: FlyoutDataset;
}
| {
type: 'SELECT_NEW_DATASET';
dataset: FlyoutDataset;
}
| {
type: 'UPDATE_INSIGHTS_TIME_RANGE';
timeRange: TimeRangeConfig;
}
| {
type: 'BREAKDOWN_FIELD_CHANGE';
breakdownField: string | null;
}
| {
type: 'CLOSE_FLYOUT';
}
| {
type: 'TOGGLE_INACTIVE_DATASETS';
}
@ -236,10 +137,7 @@ export type DatasetQualityControllerEvent =
}
| DoneInvokeEvent<DataStreamDegradedDocsStatServiceResponse>
| DoneInvokeEvent<NonAggregatableDatasets>
| DoneInvokeEvent<Dashboard[]>
| DoneInvokeEvent<DataStreamDetails>
| DoneInvokeEvent<DegradedFieldResponse>
| DoneInvokeEvent<DataStreamSettings>
| DoneInvokeEvent<DataStreamStatServiceResponse>
| DoneInvokeEvent<Integration>
| DoneInvokeEvent<boolean | null>

View file

@ -14613,7 +14613,6 @@
"xpack.datasetQuality.appTitle": "Qualité de lensemble de données",
"xpack.datasetQuality.betaBadgeDescription": "Cette fonctionnalité est actuellement en version bêta. Nous aimerions beaucoup savoir si vous avez des commentaires ou si vous rencontrez des bugs. Veuillez ouvrir un dossier d'assistance et/ou consulter notre forum de discussion.",
"xpack.datasetQuality.betaBadgeLabel": "Bêta",
"xpack.datasetQuality.collapseLabel": "Réduire",
"xpack.datasetQuality.datasetQualityColumnName": "Qualité de lensemble de données",
"xpack.datasetQuality.datasetQualityColumnTooltip": "La qualité est basée sur le pourcentage de documents dégradés dans un ensemble de données. {visualQueue}",
"xpack.datasetQuality.datasetQualityIdicator": "{quality}",
@ -14624,9 +14623,6 @@
"xpack.datasetQuality.emptyState.noData.title": "Aucun ensemble de données trouvé",
"xpack.datasetQuality.emptyState.noPrivileges.message": "Vous ne disposez pas des autorisations requises pour voir les données de logs. Assurez-vous d'avoir les autorisations requises pour voir {datasetPattern}.",
"xpack.datasetQuality.emptyState.noPrivileges.title": "Impossible de charger les ensembles de données",
"xpack.datasetQuality.expandLabel": "Développer",
"xpack.datasetQuality.fetchDatasetDetailsFailed": "Nous n'avons pas pu obtenir les détails de votre ensemble de données.",
"xpack.datasetQuality.fetchDatasetDetailsFailed.noDatasetSelected": "Vous n'avez sélectionné aucun ensemble de données",
"xpack.datasetQuality.fetchDatasetStatsFailed": "Nous n'avons pas pu obtenir vos ensembles de données.",
"xpack.datasetQuality.fetchDegradedStatsFailed": "Nous n'avons pas pu obtenir d'informations sur vos documents dégradés.",
"xpack.datasetQuality.fetchIntegrationsFailed": "Nous n'avons pas pu obtenir vos intégrations.",
@ -14634,9 +14630,6 @@
"xpack.datasetQuality.fewDegradedDocsTooltip": "{degradedDocsCount} documents dégradés dans cet ensemble de données.",
"xpack.datasetQuality.filterBar.placeholder": "Filtrer les ensembles de données",
"xpack.datasetQuality.flyout.degradedDocsTitle": "Documents dégradés",
"xpack.datasetQuality.flyout.degradedField.count": "Nombre de documents",
"xpack.datasetQuality.flyout.degradedField.field": "Champ",
"xpack.datasetQuality.flyout.degradedField.lastOccurrence": "Dernière occurrence",
"xpack.datasetQuality.flyout.nonAggregatable.description": "{description}",
"xpack.datasetQuality.flyout.nonAggregatable.howToFixIt": "{rolloverLink} manuellement cet ensemble de données pour empêcher des délais à l'avenir.",
"xpack.datasetQuality.flyout.nonAggregatable.warning": "{dataset} est incompatible avec l'agrégation _ignored, ce qui peut entraîner des délais lors de la recherche de données. {howToFixIt}",

View file

@ -14602,7 +14602,6 @@
"xpack.datasetQuality.appTitle": "データセット品質",
"xpack.datasetQuality.betaBadgeDescription": "現在、この機能はベータです。バグが発生した場合やフィードバックがある場合は、お問い合わせください。サポート問題をオープンするか、ディスカッションフォーラムをご覧ください。",
"xpack.datasetQuality.betaBadgeLabel": "ベータ",
"xpack.datasetQuality.collapseLabel": "縮小",
"xpack.datasetQuality.datasetQualityColumnName": "データセット品質",
"xpack.datasetQuality.datasetQualityColumnTooltip": "品質は、データセットの劣化したドキュメントの割合に基づきます。{visualQueue}",
"xpack.datasetQuality.datasetQualityIdicator": "{quality}",
@ -14613,9 +14612,6 @@
"xpack.datasetQuality.emptyState.noData.title": "データセットが見つかりません",
"xpack.datasetQuality.emptyState.noPrivileges.message": "ログデータを表示するために必要な権限がありません。{datasetPattern}を表示するための十分な権限があることを確認してください。",
"xpack.datasetQuality.emptyState.noPrivileges.title": "データセットを読み込めませんでした",
"xpack.datasetQuality.expandLabel": "拡張",
"xpack.datasetQuality.fetchDatasetDetailsFailed": "データセット詳細を取得できませんでした。",
"xpack.datasetQuality.fetchDatasetDetailsFailed.noDatasetSelected": "データセットが選択されていません",
"xpack.datasetQuality.fetchDatasetStatsFailed": "データセットを取得できませんでした。",
"xpack.datasetQuality.fetchDegradedStatsFailed": "劣化したドキュメント情報を取得できませんでした。",
"xpack.datasetQuality.fetchIntegrationsFailed": "統合を取得できませんでした。",
@ -14623,9 +14619,6 @@
"xpack.datasetQuality.fewDegradedDocsTooltip": "このデータセットの{degradedDocsCount}個の劣化したドキュメント。",
"xpack.datasetQuality.filterBar.placeholder": "データセットのフィルタリング",
"xpack.datasetQuality.flyout.degradedDocsTitle": "劣化したドキュメント",
"xpack.datasetQuality.flyout.degradedField.count": "ドキュメント数",
"xpack.datasetQuality.flyout.degradedField.field": "フィールド",
"xpack.datasetQuality.flyout.degradedField.lastOccurrence": "前回の発生",
"xpack.datasetQuality.flyout.nonAggregatable.description": "{description}",
"xpack.datasetQuality.flyout.nonAggregatable.howToFixIt": "今後の遅れを防止するには、手動でこのデータを{rolloverLink}してください。",
"xpack.datasetQuality.flyout.nonAggregatable.warning": "{dataset}は_ignored集約をサポートしていません。データのクエリを実行するときに遅延が生じる可能性があります。{howToFixIt}",

View file

@ -14625,7 +14625,6 @@
"xpack.datasetQuality.appTitle": "数据集质量",
"xpack.datasetQuality.betaBadgeDescription": "此功能当前为公测版。如果遇到任何错误或有任何反馈,我们乐于倾听您的意见。请报告支持问题和/或访问我们的讨论论坛。",
"xpack.datasetQuality.betaBadgeLabel": "公测版",
"xpack.datasetQuality.collapseLabel": "折叠",
"xpack.datasetQuality.datasetQualityColumnName": "数据集质量",
"xpack.datasetQuality.datasetQualityColumnTooltip": "质量基于数据集中的已降级文档的百分比。{visualQueue}",
"xpack.datasetQuality.datasetQualityIdicator": "{quality}",
@ -14636,9 +14635,6 @@
"xpack.datasetQuality.emptyState.noData.title": "找不到数据集",
"xpack.datasetQuality.emptyState.noPrivileges.message": "您没有查看日志数据所需的权限。请确保您具有足够的权限,可以查看 {datasetPattern}。",
"xpack.datasetQuality.emptyState.noPrivileges.title": "无法加载数据集",
"xpack.datasetQuality.expandLabel": "展开",
"xpack.datasetQuality.fetchDatasetDetailsFailed": "无法获取数据集详情。",
"xpack.datasetQuality.fetchDatasetDetailsFailed.noDatasetSelected": "尚未选择任何数据集",
"xpack.datasetQuality.fetchDatasetStatsFailed": "无法获取数据集。",
"xpack.datasetQuality.fetchDegradedStatsFailed": "无法获取已降级文档信息。",
"xpack.datasetQuality.fetchIntegrationsFailed": "无法获取集成。",
@ -14646,9 +14642,6 @@
"xpack.datasetQuality.fewDegradedDocsTooltip": "此数据集中的 {degradedDocsCount} 个已降级文档。",
"xpack.datasetQuality.filterBar.placeholder": "筛选数据集",
"xpack.datasetQuality.flyout.degradedDocsTitle": "已降级文档",
"xpack.datasetQuality.flyout.degradedField.count": "文档计数",
"xpack.datasetQuality.flyout.degradedField.field": "字段",
"xpack.datasetQuality.flyout.degradedField.lastOccurrence": "最后一次发生",
"xpack.datasetQuality.flyout.nonAggregatable.description": "{description}",
"xpack.datasetQuality.flyout.nonAggregatable.howToFixIt": "手动 {rolloverLink} 此数据集以防止未来出现延迟。",
"xpack.datasetQuality.flyout.nonAggregatable.warning": "{dataset} 不支持 _ignored 聚合,在查询数据时可能会导致延迟。{howToFixIt}",

View file

@ -10,6 +10,7 @@ import { DatasetQualityFtrProviderContext } from './config';
import {
createDegradedFieldsRecord,
datasetNames,
defaultNamespace,
getInitialTestLogs,
getLogsForDataset,
productionNamespace,
@ -33,8 +34,9 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
const retry = getService('retry');
const browser = getService('browser');
const to = '2024-01-01T12:00:00.000Z';
const apacheAccessDatasetName = 'apache.access';
const apacheAccessDatasetHumanName = 'Apache access logs';
const apacheAccessDataStreamName = `logs-${apacheAccessDatasetName}-${productionNamespace}`;
const apacheIntegrationId = 'apache';
const apachePkg = {
name: 'apache',
@ -42,15 +44,18 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
};
const bitbucketDatasetName = 'atlassian_bitbucket.audit';
const bitbucketDatasetHumanName = 'Bitbucket Audit Logs';
const bitbucketAuditDataStreamName = `logs-${bitbucketDatasetName}-${defaultNamespace}`;
const bitbucketPkg = {
name: 'atlassian_bitbucket',
version: '1.14.0',
};
const regularDatasetName = datasetNames[0];
const regularDataStreamName = `logs-${datasetNames[0]}-${defaultNamespace}`;
const degradedDatasetName = datasetNames[2];
const degradedDataStreamName = `logs-${degradedDatasetName}-${defaultNamespace}`;
describe('Flyout', () => {
describe('Dataset Quality Details', () => {
before(async () => {
// Install Apache Integration and ingest logs for it
await PageObjects.observabilityLogsExplorer.installPackage(apachePkg);
@ -85,8 +90,6 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
// Index logs for Bitbucket integration
getLogsForDataset({ to, count: 10, dataset: bitbucketDatasetName }),
]);
await PageObjects.datasetQuality.navigateTo();
});
after(async () => {
@ -95,21 +98,49 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
await synthtrace.clean();
});
describe('open flyout', () => {
it('should open the flyout for the right dataset', async () => {
const testDatasetName = datasetNames[1];
await PageObjects.datasetQuality.openDatasetFlyout(testDatasetName);
describe('navigate to dataset details', () => {
it('should navigate to right dataset', async () => {
await PageObjects.datasetQuality.navigateToDetails({ dataStream: regularDataStreamName });
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutTitle
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsTitle
);
});
it('should navigate to details page from a main page', async () => {
await PageObjects.datasetQuality.navigateTo();
const synthDataset = await testSubjects.find(
'datasetQualityTableDetailsLink-logs-synth.1-default',
20 * 1000
);
await PageObjects.datasetQuality.closeFlyout();
await synthDataset.click();
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsTitle
);
});
it('should show an empty prompt with error message when the dataset is not found', async () => {
const nonExistentDataStreamName = 'logs-non.existent-production';
await PageObjects.datasetQuality.navigateToDetails({
dataStream: nonExistentDataStreamName,
});
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsEmptyPrompt
);
const emptyPromptBody = await testSubjects.getVisibleText(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsEmptyPromptBody
);
expect(emptyPromptBody).to.contain(nonExistentDataStreamName);
});
it('reflects the breakdown field state in url', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({ dataStream: degradedDataStreamName });
const breakdownField = 'service.name';
await PageObjects.datasetQuality.selectBreakdownField(breakdownField);
@ -128,46 +159,72 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
const currentUrl = await browser.getCurrentUrl();
expect(currentUrl).to.not.contain('breakdownField');
});
await PageObjects.datasetQuality.closeFlyout();
});
});
describe('integrations', () => {
describe('overview summary panel', () => {
it('should show summary KPIs', async () => {
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
const { docsCountTotal, degradedDocs, services, hosts, size } =
await PageObjects.datasetQuality.parseOverviewSummaryPanelKpis();
expect(parseInt(docsCountTotal, 10)).to.be(226);
expect(parseInt(degradedDocs, 10)).to.be(1);
expect(parseInt(services, 10)).to.be(3);
expect(parseInt(hosts, 10)).to.be(52);
expect(parseInt(size, 10)).to.be.greaterThan(0);
});
});
describe('overview integrations', () => {
it('should hide the integration section for non integrations', async () => {
const testDatasetName = datasetNames[1];
await PageObjects.datasetQuality.openDatasetFlyout(testDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: regularDataStreamName,
});
// The Integration row should not be present
await testSubjects.missingOrFail(
PageObjects.datasetQuality.testSubjectSelectors
.datasetQualityFlyoutFieldsListIntegrationDetails
.datasetQualityDetailsIntegrationRowIntegration
);
await PageObjects.datasetQuality.closeFlyout();
// The Version row should not be present
await testSubjects.missingOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsIntegrationRowVersion
);
});
it('should shows the integration section for integrations', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors
.datasetQualityFlyoutFieldsListIntegrationDetails
.datasetQualityDetailsIntegrationRowIntegration
);
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsIntegrationRowVersion
);
await retry.tryForTime(5000, async () => {
const integrationNameExists = await PageObjects.datasetQuality.doesTextExist(
PageObjects.datasetQuality.testSubjectSelectors
.datasetQualityFlyoutFieldsListIntegrationDetails,
.datasetQualityDetailsIntegrationRowIntegration,
apacheIntegrationId
);
expect(integrationNameExists).to.be(true);
});
await PageObjects.datasetQuality.closeFlyout();
});
it('should show the integration actions menu with correct actions', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
const actions = await Promise.all(
@ -177,23 +234,26 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
);
expect(actions.length).to.eql(3);
await PageObjects.datasetQuality.closeFlyout();
});
it('should hide integration dashboard for integrations without dashboards', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(bitbucketDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: bitbucketAuditDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
await testSubjects.missingOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutIntegrationAction(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsIntegrationAction(
integrationActions.viewDashboards
)
);
await PageObjects.datasetQuality.closeFlyout();
});
it('Should navigate to integration overview page on clicking integration overview action', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(bitbucketDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: bitbucketAuditDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
const action = await PageObjects.datasetQuality.getIntegrationActionButtonByAction(
@ -208,12 +268,12 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
expect(parsedUrl.pathname).to.contain('/app/integrations/detail/atlassian_bitbucket');
});
await PageObjects.datasetQuality.navigateTo();
});
it('should navigate to index template page in clicking Integration template', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
const action = await PageObjects.datasetQuality.getIntegrationActionButtonByAction(
@ -229,11 +289,12 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
`/app/management/data/index_management/templates/logs-${apacheAccessDatasetName}`
);
});
await PageObjects.datasetQuality.navigateTo();
});
it('should navigate to the selected dashboard on clicking integration dashboard action ', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
const action = await PageObjects.datasetQuality.getIntegrationActionButtonByAction(
@ -251,119 +312,87 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
const breadcrumbText = await testSubjects.getVisibleText('breadcrumb last');
expect(breadcrumbText).to.eql(dashboardText);
await PageObjects.datasetQuality.navigateTo();
});
});
describe('summary panel', () => {
it('should show summary KPIs', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
const { docsCountTotal, degradedDocs, services, hosts, size } =
await PageObjects.datasetQuality.parseFlyoutKpis();
expect(parseInt(docsCountTotal, 10)).to.be(226);
expect(parseInt(degradedDocs, 10)).to.be(1);
expect(parseInt(services, 10)).to.be(3);
expect(parseInt(hosts, 10)).to.be(52);
expect(parseInt(size, 10)).to.be.greaterThan(0);
await PageObjects.datasetQuality.closeFlyout();
});
});
describe('navigation', () => {
afterEach(async () => {
// Navigate back to dataset quality page after each test
await PageObjects.datasetQuality.navigateTo();
});
it('should go to log explorer page when the open in log explorer button is clicked', async () => {
const testDatasetName = datasetNames[2];
await PageObjects.datasetQuality.openDatasetFlyout(testDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: regularDataStreamName,
});
const logExplorerButton = await PageObjects.datasetQuality.getFlyoutLogsExplorerButton();
const logExplorerButton =
await PageObjects.datasetQuality.getDatasetQualityDetailsHeaderButton();
await logExplorerButton.click();
// Confirm dataset selector text in observability logs explorer
const datasetSelectorText =
await PageObjects.observabilityLogsExplorer.getDataSourceSelectorButtonText();
expect(datasetSelectorText).to.eql(testDatasetName);
expect(datasetSelectorText).to.eql(regularDatasetName);
});
it('should go log explorer for degraded docs when the show all button is clicked', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
it('should go log explorer for degraded docs when the button next to breakdown selector is clicked', async () => {
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
const degradedDocsShowAllSelector = `${PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutKpiLink}-${PageObjects.datasetQuality.texts.degradedDocs}`;
await testSubjects.click(degradedDocsShowAllSelector);
await testSubjects.click(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsLinkToDiscover
);
// Confirm dataset selector text in observability logs explorer
const datasetSelectorText =
await PageObjects.observabilityLogsExplorer.getDataSourceSelectorButtonText();
expect(datasetSelectorText).to.contain(apacheAccessDatasetName);
});
// Blocked by https://github.com/elastic/kibana/issues/181705
// Its a test written ahead of its time.
it.skip('goes to infra hosts for hosts when show all is clicked', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
const hostsShowAllSelector = `${PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutKpiLink}-${PageObjects.datasetQuality.texts.hosts}`;
await testSubjects.click(hostsShowAllSelector);
// Confirm url contains metrics/hosts
await retry.tryForTime(5000, async () => {
const currentUrl = await browser.getCurrentUrl();
const parsedUrl = new URL(currentUrl);
expect(parsedUrl.pathname).to.contain('/app/metrics/hosts');
});
});
});
describe('degraded fields table', () => {
it(' should show empty degraded fields table when no degraded fields are present', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(datasetNames[0]);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: regularDataStreamName,
});
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutDegradedTableNoData
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedTableNoData
);
await PageObjects.datasetQuality.closeFlyout();
});
it('should show the degraded fields table with data when present', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: degradedDataStreamName,
});
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutDegradedFieldTable
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldTable
);
const rows =
await PageObjects.datasetQuality.getDatasetQualityFlyoutDegradedFieldTableRows();
await PageObjects.datasetQuality.getDatasetQualityDetailsDegradedFieldTableRows();
expect(rows.length).to.eql(2);
await PageObjects.datasetQuality.closeFlyout();
});
it('should display Spark Plot for every row of degraded fields', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: degradedDataStreamName,
});
const rows =
await PageObjects.datasetQuality.getDatasetQualityFlyoutDegradedFieldTableRows();
await PageObjects.datasetQuality.getDatasetQualityDetailsDegradedFieldTableRows();
const sparkPlots = await testSubjects.findAll(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualitySparkPlot
);
expect(rows.length).to.be(sparkPlots.length);
await PageObjects.datasetQuality.closeFlyout();
});
it('should sort the table when the count table header is clicked', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: degradedDataStreamName,
});
const table = await PageObjects.datasetQuality.parseDegradedFieldTable();
@ -374,12 +403,12 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
const sortedCellTexts = await countColumn.getCellTexts();
expect(cellTexts.reverse()).to.eql(sortedCellTexts);
await PageObjects.datasetQuality.closeFlyout();
});
it('should update the URL when the table is sorted', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: degradedDataStreamName,
});
const table = await PageObjects.datasetQuality.parseDegradedFieldTable();
const countColumn = table['Docs count'];
@ -405,8 +434,6 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
'sort:(direction:asc,field:count)'
);
});
await PageObjects.datasetQuality.closeFlyout();
});
// This is the only test which ingest data during the test.
@ -414,7 +441,9 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
// Even though this test ingest data, it can also be freely moved inside
// this describe block, and it won't affect any of the existing tests
it('should update the table when new data is ingested and the flyout is refreshed using the time selector', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: degradedDataStreamName,
});
const table = await PageObjects.datasetQuality.parseDegradedFieldTable();
@ -429,7 +458,7 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
}),
]);
await PageObjects.datasetQuality.refreshFlyout();
await PageObjects.datasetQuality.refreshDetailsPageData();
const updatedTable = await PageObjects.datasetQuality.parseDegradedFieldTable();
const updatedCountColumn = updatedTable['Docs count'];
@ -440,8 +469,6 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
const singleValueNow = parseInt(updatedCellTexts[0], 10);
expect(singleValueNow).to.be.greaterThan(singleValuePreviously);
await PageObjects.datasetQuality.closeFlyout();
});
});
});

View file

@ -7,7 +7,7 @@
import expect from '@kbn/expect';
import { DatasetQualityFtrProviderContext } from './config';
import { getInitialTestLogs, getLogsForDataset } from './data';
import { datasetNames, defaultNamespace, getInitialTestLogs, getLogsForDataset } from './data';
export default function ({ getService, getPageObjects }: DatasetQualityFtrProviderContext) {
const PageObjects = getPageObjects([
@ -25,6 +25,8 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
const apacheAccessDatasetName = 'apache.access';
const apacheAccessDatasetHumanName = 'Apache access logs';
const regularDataStreamName = `logs-${datasetNames[0]}-${defaultNamespace}`;
const apacheAccessDataStreamName = `logs-${apacheAccessDatasetName}-${defaultNamespace}`;
describe('Dataset quality handles user privileges', () => {
before(async () => {
@ -170,35 +172,39 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid
);
});
it('flyout shows insufficient privileges warning for underprivileged data stream', async () => {
await PageObjects.datasetQuality.openDatasetFlyout('synth.1');
it('Details page shows insufficient privileges warning for underprivileged data stream', async () => {
await PageObjects.datasetQuality.navigateToDetails({
dataStream: regularDataStreamName,
});
await testSubjects.existOrFail(
`${PageObjects.datasetQuality.testSubjectSelectors.datasetQualityInsufficientPrivileges}-Size`
);
await PageObjects.datasetQuality.closeFlyout();
await PageObjects.datasetQuality.navigateTo();
});
it('"View dashboards" and "See integration" are hidden for underprivileged user', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
// "See Integration" is hidden
await testSubjects.missingOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutIntegrationAction(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsIntegrationAction(
'Overview'
)
);
// "View Dashboards" is hidden
await testSubjects.missingOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutIntegrationAction(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsIntegrationAction(
'ViewDashboards'
)
);
await PageObjects.datasetQuality.closeFlyout();
await PageObjects.datasetQuality.navigateTo();
});
});
});

View file

@ -13,7 +13,7 @@ export default function ({ loadTestFile }: DatasetQualityFtrProviderContext) {
loadTestFile(require.resolve('./dataset_quality_summary'));
loadTestFile(require.resolve('./dataset_quality_table'));
loadTestFile(require.resolve('./dataset_quality_table_filters'));
loadTestFile(require.resolve('./dataset_quality_flyout'));
loadTestFile(require.resolve('./dataset_quality_privileges'));
loadTestFile(require.resolve('./dataset_quality_details'));
});
}

View file

@ -7,12 +7,11 @@
import querystring from 'querystring';
import rison from '@kbn/rison';
import expect from '@kbn/expect';
import { TimeUnitId } from '@elastic/eui';
import { WebElementWrapper } from '@kbn/ftr-common-functional-ui-services';
import {
DATA_QUALITY_URL_STATE_KEY,
datasetQualityUrlSchemaV1,
datasetQualityDetailsUrlSchemaV1,
} from '@kbn/data-quality-plugin/common';
import {
DEFAULT_DEGRADED_FIELD_SORT_DIRECTION,
@ -26,15 +25,18 @@ const defaultPageState: datasetQualityUrlSchemaV1.UrlSchema = {
page: 0,
},
filters: {},
flyout: {
degradedFields: {
table: {
page: 0,
rowsPerPage: 10,
sort: {
field: DEFAULT_DEGRADED_FIELD_SORT_FIELD,
direction: DEFAULT_DEGRADED_FIELD_SORT_DIRECTION,
},
};
const defaultDetailsPageState: datasetQualityDetailsUrlSchemaV1.UrlSchema = {
v: 1,
dataStream: 'logs-synth.1-default',
degradedFields: {
table: {
page: 0,
rowsPerPage: 10,
sort: {
field: DEFAULT_DEGRADED_FIELD_SORT_FIELD,
direction: DEFAULT_DEGRADED_FIELD_SORT_DIRECTION,
},
},
},
@ -49,7 +51,10 @@ type SummaryPanelKpi = Record<
string
>;
type FlyoutKpi = Record<'docsCountTotal' | 'size' | 'services' | 'hosts' | 'degradedDocs', string>;
type SummaryPanelKPI = Record<
'docsCountTotal' | 'size' | 'services' | 'hosts' | 'degradedDocs',
string
>;
const texts = {
noActivityText: 'No activity in the selected timeframe',
@ -58,7 +63,7 @@ const texts = {
datasetHealthGood: 'Good',
activeDatasets: 'Active Data Sets',
estimatedData: 'Estimated Data',
docsCountTotal: 'Docs count (total)',
docsCountTotal: 'Total count',
size: 'Size',
services: 'Services',
hosts: 'Hosts',
@ -86,20 +91,16 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv
datasetQualityTable: 'datasetQualityTable',
datasetQualityFiltersContainer: 'datasetQualityFiltersContainer',
datasetQualityExpandButton: 'datasetQualityExpandButton',
datasetQualityFlyout: 'datasetQualityFlyout',
datasetQualityFlyoutBody: 'datasetQualityFlyoutBody',
datasetQualityFlyoutTitle: 'datasetQualityFlyoutTitle',
datasetQualityFlyoutDegradedFieldTable: 'datasetQualityFlyoutDegradedFieldTable',
datasetQualityFlyoutDegradedTableNoData: 'datasetQualityFlyoutDegradedTableNoData',
datasetDetailsContainer: 'datasetDetailsContainer',
datasetQualityDetailsTitle: 'datasetQualityDetailsTitle',
datasetQualityDetailsDegradedFieldTable: 'datasetQualityDetailsDegradedFieldTable',
datasetQualityDetailsDegradedTableNoData: 'datasetQualityDetailsDegradedTableNoData',
datasetQualitySparkPlot: 'datasetQualitySparkPlot',
datasetQualityHeaderButton: 'datasetQualityHeaderButton',
datasetQualityFlyoutFieldValue: 'datasetQualityFlyoutFieldValue',
datasetQualityFlyoutFieldsListIntegrationDetails:
'datasetQualityFlyoutFieldsList-integration_details',
datasetQualityFlyoutIntegrationLoading: 'datasetQualityFlyoutIntegrationLoading',
datasetQualityFlyoutIntegrationActionsButton: 'datasetQualityFlyoutIntegrationActionsButton',
datasetQualityFlyoutIntegrationAction: (action: string) =>
`datasetQualityFlyoutIntegrationAction${action}`,
datasetQualityDetailsHeaderButton: 'datasetQualityDetailsHeaderButton',
datasetQualityDetailsIntegrationLoading: 'datasetQualityDetailsIntegrationLoading',
datasetQualityDetailsIntegrationActionsButton: 'datasetQualityDetailsIntegrationActionsButton',
datasetQualityDetailsIntegrationAction: (action: string) =>
`datasetQualityDetailsIntegrationAction${action}`,
datasetQualityFilterBarFieldSearch: 'datasetQualityFilterBarFieldSearch',
datasetQualityIntegrationsSelectable: 'datasetQualityIntegrationsSelectable',
datasetQualityIntegrationsSelectableButton: 'datasetQualityIntegrationsSelectableButton',
@ -107,9 +108,13 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv
datasetQualityNamespacesSelectableButton: 'datasetQualityNamespacesSelectableButton',
datasetQualityQualitiesSelectable: 'datasetQualityQualitiesSelectable',
datasetQualityQualitiesSelectableButton: 'datasetQualityQualitiesSelectableButton',
datasetQualityDetailsEmptyPrompt: 'datasetQualityDetailsEmptyPrompt',
datasetQualityDetailsEmptyPromptBody: 'datasetQualityDetailsEmptyPromptBody',
datasetQualityDatasetHealthKpi: 'datasetQualityDatasetHealthKpi',
datasetQualityFlyoutKpiValue: 'datasetQualityFlyoutKpiValue',
datasetQualityFlyoutKpiLink: 'datasetQualityFlyoutKpiLink',
datasetQualityDetailsSummaryKpiValue: 'datasetQualityDetailsSummaryKpiValue',
datasetQualityDetailsIntegrationRowIntegration: 'datasetQualityDetailsFieldsList-integration',
datasetQualityDetailsIntegrationRowVersion: 'datasetQualityDetailsFieldsList-version',
datasetQualityDetailsLinkToDiscover: 'datasetQualityDetailsLinkToDiscover',
datasetQualityInsufficientPrivileges: 'datasetQualityInsufficientPrivileges',
datasetQualityNoDataEmptyState: 'datasetQualityNoDataEmptyState',
datasetQualityNoPrivilegesEmptyState: 'datasetQualityNoPrivilegesEmptyState',
@ -117,7 +122,6 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv
superDatePickerToggleQuickMenuButton: 'superDatePickerToggleQuickMenuButton',
superDatePickerApplyTimeButton: 'superDatePickerApplyTimeButton',
superDatePickerQuickMenu: 'superDatePickerQuickMenu',
euiFlyoutCloseButton: 'euiFlyoutCloseButton',
unifiedHistogramBreakdownSelectorButton: 'unifiedHistogramBreakdownSelectorButton',
unifiedHistogramBreakdownSelectorSelectorSearch:
'unifiedHistogramBreakdownSelectorSelectorSearch',
@ -156,21 +160,32 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv
);
},
async navigateToDetails(pageState: datasetQualityDetailsUrlSchemaV1.UrlSchema) {
const queryStringParams = querystring.stringify({
[DATA_QUALITY_URL_STATE_KEY]: rison.encode(
datasetQualityDetailsUrlSchemaV1.urlSchemaRT.encode({
...defaultDetailsPageState,
...pageState,
})
),
});
return PageObjects.common.navigateToUrlWithBrowserHistory(
'management',
'/data/data_quality/details',
queryStringParams,
{
// the check sometimes is too slow for the page so it misses the point
// in time before the app rewrites the URL
ensureCurrentUrl: false,
}
);
},
async waitUntilTableLoaded() {
await find.waitForDeletedByCssSelector('.euiBasicTable-loading', 20 * 1000);
},
async waitUntilTableInFlyoutLoaded() {
await find.waitForDeletedByCssSelector('.euiFlyoutBody .euiBasicTable-loading', 20 * 1000);
},
async waitUntilIntegrationsInFlyoutLoaded() {
await find.waitForDeletedByCssSelector(
'.euiSkeletonTitle .datasetQualityFlyoutIntegrationLoading',
10 * 1000
);
},
async waitUntilSummaryPanelLoaded(isStateful: boolean = true) {
await testSubjects.missingOrFail(`datasetQuality-${texts.activeDatasets}-loading`);
if (isStateful) {
@ -213,14 +228,14 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv
return testSubjects.find(testSubjectSelectors.datasetQualityTable);
},
getDatasetQualityFlyoutDegradedFieldTable(): Promise<WebElementWrapper> {
return testSubjects.find(testSubjectSelectors.datasetQualityFlyoutDegradedFieldTable);
getDatasetQualityDetailsDegradedFieldTable(): Promise<WebElementWrapper> {
return testSubjects.find(testSubjectSelectors.datasetQualityDetailsDegradedFieldTable);
},
async getDatasetQualityFlyoutDegradedFieldTableRows(): Promise<WebElementWrapper[]> {
await this.waitUntilTableInFlyoutLoaded();
async getDatasetQualityDetailsDegradedFieldTableRows(): Promise<WebElementWrapper[]> {
await this.waitUntilTableLoaded();
const table = await testSubjects.find(
testSubjectSelectors.datasetQualityFlyoutDegradedFieldTable
testSubjectSelectors.datasetQualityDetailsDegradedFieldTable
);
const tBody = await table.findByTagName('tbody');
return tBody.findAllByTagName('tr');
@ -265,8 +280,8 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv
},
async parseDegradedFieldTable() {
await this.waitUntilTableInFlyoutLoaded();
const table = await this.getDatasetQualityFlyoutDegradedFieldTable();
await this.waitUntilTableLoaded();
const table = await this.getDatasetQualityDetailsDegradedFieldTable();
return this.parseTable(table, ['Field', 'Docs count', 'Last Occurrence']);
},
@ -302,46 +317,11 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv
return find.clickByCssSelector(selectors.showFullDatasetNamesSwitch);
},
async openDatasetFlyout(datasetName: string) {
await this.waitUntilTableLoaded();
const cols = await this.parseDatasetTable();
const datasetNameCol = cols['Data Set Name'];
const datasetNameColCellTexts = await datasetNameCol.getCellTexts();
const testDatasetRowIndex = datasetNameColCellTexts.findIndex(
(dName) => dName === datasetName
async refreshDetailsPageData() {
const datasetDetailsContainer: WebElementWrapper = await testSubjects.find(
testSubjectSelectors.datasetDetailsContainer
);
expect(testDatasetRowIndex).to.be.greaterThan(-1);
const expandColumn = cols['0'];
const expandButtons = await expandColumn.getCellChildren(
`[data-test-subj=${testSubjectSelectors.datasetQualityExpandButton}]`
);
expect(expandButtons.length).to.be.greaterThan(0);
const datasetExpandButton = expandButtons[testDatasetRowIndex];
// Check if 'title' attribute is "Expand" or "Collapse"
const isCollapsed = (await datasetExpandButton.getAttribute('title')) === 'Expand';
// Open if collapsed
if (isCollapsed) {
await datasetExpandButton.click();
}
await this.waitUntilIntegrationsInFlyoutLoaded();
},
async closeFlyout() {
return testSubjects.click(testSubjectSelectors.euiFlyoutCloseButton);
},
async refreshFlyout() {
const flyoutContainer: WebElementWrapper = await testSubjects.find(
testSubjectSelectors.datasetQualityFlyoutBody
);
const refreshButton = await flyoutContainer.findByTestSubject(
const refreshButton = await datasetDetailsContainer.findByTestSubject(
testSubjectSelectors.superDatePickerApplyTimeButton
);
return refreshButton.click();
@ -357,27 +337,27 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv
return false;
},
getFlyoutLogsExplorerButton() {
return testSubjects.find(testSubjectSelectors.datasetQualityHeaderButton);
getDatasetQualityDetailsHeaderButton() {
return testSubjects.find(testSubjectSelectors.datasetQualityDetailsHeaderButton);
},
openIntegrationActionsMenu() {
return testSubjects.click(testSubjectSelectors.datasetQualityFlyoutIntegrationActionsButton);
return testSubjects.click(testSubjectSelectors.datasetQualityDetailsIntegrationActionsButton);
},
getIntegrationActionButtonByAction(action: string) {
return testSubjects.find(testSubjectSelectors.datasetQualityFlyoutIntegrationAction(action));
return testSubjects.find(testSubjectSelectors.datasetQualityDetailsIntegrationAction(action));
},
getIntegrationDashboardButtons() {
return testSubjects.findAll(
testSubjectSelectors.datasetQualityFlyoutIntegrationAction('Dashboard')
testSubjectSelectors.datasetQualityDetailsIntegrationAction('Dashboard')
);
},
// `excludeKeys` needed to circumvent `_stats` not available in Serverless https://github.com/elastic/kibana/issues/178954
// TODO: Remove `excludeKeys` when `_stats` is available in Serverless
async parseFlyoutKpis(excludeKeys: string[] = []): Promise<FlyoutKpi> {
async parseOverviewSummaryPanelKpis(excludeKeys: string[] = []): Promise<SummaryPanelKPI> {
const kpiTitleAndKeys = [
{ title: texts.docsCountTotal, key: 'docsCountTotal' },
{ title: texts.size, key: 'size' },
@ -390,7 +370,7 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv
kpiTitleAndKeys.map(async ({ title, key }) => ({
key,
value: await testSubjects.getVisibleText(
`${testSubjectSelectors.datasetQualityFlyoutKpiValue}-${title}`
`${testSubjectSelectors.datasetQualityDetailsSummaryKpiValue}-${title}`
),
}))
);
@ -400,52 +380,10 @@ export function DatasetQualityPageObject({ getPageObjects, getService }: FtrProv
...acc,
[key]: value,
}),
{} as FlyoutKpi
{} as SummaryPanelKPI
);
},
async setDatePickerLastXUnits(
container: WebElementWrapper,
timeValue: number,
unit: TimeUnitId
) {
// Only click the menu button found under the provided container
const datePickerToggleQuickMenuButton = await container.findByTestSubject(
testSubjectSelectors.superDatePickerToggleQuickMenuButton
);
await datePickerToggleQuickMenuButton.click();
const datePickerQuickMenu = await testSubjects.find(
testSubjectSelectors.superDatePickerQuickMenu
);
const timeTenseSelect = await datePickerQuickMenu.findByCssSelector(
`select[aria-label="Time tense"]`
);
const timeValueInput = await datePickerQuickMenu.findByCssSelector(
`input[aria-label="Time value"]`
);
const timeUnitSelect = await datePickerQuickMenu.findByCssSelector(
`select[aria-label="Time unit"]`
);
await timeTenseSelect.focus();
await timeTenseSelect.type('Last');
await timeValueInput.focus();
await timeValueInput.clearValue();
await timeValueInput.type(timeValue.toString());
await timeUnitSelect.focus();
await timeUnitSelect.type(unit);
await (
await datePickerQuickMenu.findByCssSelector(selectors.superDatePickerApplyButton)
).click();
return testSubjects.missingOrFail(testSubjectSelectors.superDatePickerQuickMenu);
},
/**
* Selects a breakdown field from the unified histogram breakdown selector
* @param fieldText The text of the field to select. Use 'No breakdown' to clear the selection

View file

@ -6,6 +6,7 @@
*/
import expect from '@kbn/expect';
import { defaultNamespace } from '@kbn/test-suites-xpack/functional/apps/dataset_quality/data';
import { FtrProviderContext } from '../../../ftr_provider_context';
import {
datasetNames,
@ -38,7 +39,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const excludeKeysFromServerless = ['size']; // https://github.com/elastic/kibana/issues/178954
const apacheAccessDatasetName = 'apache.access';
const apacheAccessDatasetHumanName = 'Apache access logs';
const apacheAccessDataStreamName = `logs-${apacheAccessDatasetName}-${productionNamespace}`;
const apacheIntegrationId = 'apache';
const apachePkg = {
name: 'apache',
@ -46,13 +47,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
};
const bitbucketDatasetName = 'atlassian_bitbucket.audit';
const bitbucketDatasetHumanName = 'Bitbucket Audit Logs';
const bitbucketAuditDataStreamName = `logs-${bitbucketDatasetName}-${defaultNamespace}`;
const bitbucketPkg = {
name: 'atlassian_bitbucket',
version: '1.14.0',
};
const regularDatasetName = datasetNames[0];
const regularDataStreamName = `logs-${datasetNames[0]}-${defaultNamespace}`;
const degradedDatasetName = datasetNames[2];
const degradedDataStreamName = `logs-${degradedDatasetName}-${defaultNamespace}`;
describe('Flyout', function () {
before(async () => {
@ -91,8 +95,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
]);
await PageObjects.svlCommonPage.loginWithPrivilegedRole();
await PageObjects.datasetQuality.navigateTo();
});
after(async () => {
@ -101,21 +103,49 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await synthtrace.clean();
});
describe('open flyout', () => {
it('should open the flyout for the right dataset', async () => {
const testDatasetName = datasetNames[1];
await PageObjects.datasetQuality.openDatasetFlyout(testDatasetName);
describe('navigate to dataset details', () => {
it('should navigate to right dataset', async () => {
await PageObjects.datasetQuality.navigateToDetails({ dataStream: regularDataStreamName });
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutTitle
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsTitle
);
});
it('should navigate to details page from a main page', async () => {
await PageObjects.datasetQuality.navigateTo();
const synthDataset = await testSubjects.find(
'datasetQualityTableDetailsLink-logs-synth.1-default',
20 * 1000
);
await PageObjects.datasetQuality.closeFlyout();
await synthDataset.click();
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsTitle
);
});
it('should show an empty prompt with error message when the dataset is not found', async () => {
const nonExistentDataStreamName = 'logs-non.existent-production';
await PageObjects.datasetQuality.navigateToDetails({
dataStream: nonExistentDataStreamName,
});
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsEmptyPrompt
);
const emptyPromptBody = await testSubjects.getVisibleText(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsEmptyPromptBody
);
expect(emptyPromptBody).to.contain(nonExistentDataStreamName);
});
it('reflects the breakdown field state in url', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({ dataStream: degradedDataStreamName });
const breakdownField = 'service.name';
await PageObjects.datasetQuality.selectBreakdownField(breakdownField);
@ -134,46 +164,71 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const currentUrl = await browser.getCurrentUrl();
expect(currentUrl).to.not.contain('breakdownField');
});
await PageObjects.datasetQuality.closeFlyout();
});
});
describe('integrations', () => {
describe('overview summary panel', () => {
it('should show summary KPIs', async () => {
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
const { docsCountTotal, degradedDocs, services, hosts } =
await PageObjects.datasetQuality.parseOverviewSummaryPanelKpis(excludeKeysFromServerless);
expect(parseInt(docsCountTotal, 10)).to.be(226);
expect(parseInt(degradedDocs, 10)).to.be(1);
expect(parseInt(services, 10)).to.be(3);
expect(parseInt(hosts, 10)).to.be(52);
});
});
describe('overview integrations', () => {
it('should hide the integration section for non integrations', async () => {
const testDatasetName = datasetNames[1];
await PageObjects.datasetQuality.openDatasetFlyout(testDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: regularDataStreamName,
});
// The Integration row should not be present
await testSubjects.missingOrFail(
PageObjects.datasetQuality.testSubjectSelectors
.datasetQualityFlyoutFieldsListIntegrationDetails
.datasetQualityDetailsIntegrationRowIntegration
);
await PageObjects.datasetQuality.closeFlyout();
// The Version row should not be present
await testSubjects.missingOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsIntegrationRowVersion
);
});
it('should shows the integration section for integrations', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors
.datasetQualityFlyoutFieldsListIntegrationDetails
.datasetQualityDetailsIntegrationRowIntegration
);
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsIntegrationRowVersion
);
await retry.tryForTime(5000, async () => {
const integrationNameExists = await PageObjects.datasetQuality.doesTextExist(
PageObjects.datasetQuality.testSubjectSelectors
.datasetQualityFlyoutFieldsListIntegrationDetails,
.datasetQualityDetailsIntegrationRowIntegration,
apacheIntegrationId
);
expect(integrationNameExists).to.be(true);
});
await PageObjects.datasetQuality.closeFlyout();
});
it('should show the integration actions menu with correct actions', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
const actions = await Promise.all(
@ -183,23 +238,26 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
);
expect(actions.length).to.eql(3);
await PageObjects.datasetQuality.closeFlyout();
});
it('should hide integration dashboard for integrations without dashboards', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(bitbucketDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: bitbucketAuditDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
await testSubjects.missingOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutIntegrationAction(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsIntegrationAction(
integrationActions.viewDashboards
)
);
await PageObjects.datasetQuality.closeFlyout();
});
it('Should navigate to integration overview page on clicking integration overview action', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(bitbucketDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: bitbucketAuditDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
const action = await PageObjects.datasetQuality.getIntegrationActionButtonByAction(
@ -214,12 +272,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(parsedUrl.pathname).to.contain('/app/integrations/detail/atlassian_bitbucket');
});
await PageObjects.datasetQuality.navigateTo();
});
it('should navigate to index template page in clicking Integration template', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
const action = await PageObjects.datasetQuality.getIntegrationActionButtonByAction(
@ -235,11 +293,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
`/app/management/data/index_management/templates/logs-${apacheAccessDatasetName}`
);
});
await PageObjects.datasetQuality.navigateTo();
});
it('should navigate to the selected dashboard on clicking integration dashboard action ', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
await PageObjects.datasetQuality.openIntegrationActionsMenu();
const action = await PageObjects.datasetQuality.getIntegrationActionButtonByAction(
@ -257,118 +316,87 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const breadcrumbText = await testSubjects.getVisibleText('breadcrumb last');
expect(breadcrumbText).to.eql(dashboardText);
await PageObjects.datasetQuality.navigateTo();
});
});
describe('summary panel', () => {
it('should show summary KPIs', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
const { docsCountTotal, degradedDocs, services, hosts } =
await PageObjects.datasetQuality.parseFlyoutKpis(excludeKeysFromServerless);
expect(parseInt(docsCountTotal, 10)).to.be(226);
expect(parseInt(degradedDocs, 10)).to.be(1);
expect(parseInt(services, 10)).to.be(3);
expect(parseInt(hosts, 10)).to.be(52);
await PageObjects.datasetQuality.closeFlyout();
});
});
describe('navigation', () => {
afterEach(async () => {
// Navigate back to dataset quality page after each test
await PageObjects.datasetQuality.navigateTo();
});
it('should go to log explorer page when the open in log explorer button is clicked', async () => {
const testDatasetName = datasetNames[2];
await PageObjects.datasetQuality.openDatasetFlyout(testDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: regularDataStreamName,
});
const logExplorerButton = await PageObjects.datasetQuality.getFlyoutLogsExplorerButton();
const logExplorerButton =
await PageObjects.datasetQuality.getDatasetQualityDetailsHeaderButton();
await logExplorerButton.click();
// Confirm dataset selector text in observability logs explorer
const datasetSelectorText =
await PageObjects.observabilityLogsExplorer.getDataSourceSelectorButtonText();
expect(datasetSelectorText).to.eql(testDatasetName);
expect(datasetSelectorText).to.eql(regularDatasetName);
});
it('should go log explorer for degraded docs when the show all button is clicked', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
it('should go log explorer for degraded docs when the button next to breakdown selector is clicked', async () => {
await PageObjects.datasetQuality.navigateToDetails({
dataStream: apacheAccessDataStreamName,
});
const degradedDocsShowAllSelector = `${PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutKpiLink}-${PageObjects.datasetQuality.texts.degradedDocs}`;
await testSubjects.click(degradedDocsShowAllSelector);
await testSubjects.click(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsLinkToDiscover
);
// Confirm dataset selector text in observability logs explorer
const datasetSelectorText =
await PageObjects.observabilityLogsExplorer.getDataSourceSelectorButtonText();
expect(datasetSelectorText).to.contain(apacheAccessDatasetName);
});
// Blocked by https://github.com/elastic/kibana/issues/181705
// Its a test written ahead of its time.
it.skip('goes to infra hosts for hosts when show all is clicked', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(apacheAccessDatasetHumanName);
const hostsShowAllSelector = `${PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutKpiLink}-${PageObjects.datasetQuality.texts.hosts}`;
await testSubjects.click(hostsShowAllSelector);
// Confirm url contains metrics/hosts
await retry.tryForTime(5000, async () => {
const currentUrl = await browser.getCurrentUrl();
const parsedUrl = new URL(currentUrl);
expect(parsedUrl.pathname).to.contain('/app/metrics/hosts');
});
});
});
describe('degraded fields table', () => {
it(' should show empty degraded fields table when no degraded fields are present', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(datasetNames[0]);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: regularDataStreamName,
});
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutDegradedTableNoData
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedTableNoData
);
await PageObjects.datasetQuality.closeFlyout();
});
it('should show the degraded fields table with data when present', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: degradedDataStreamName,
});
await testSubjects.existOrFail(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityFlyoutDegradedFieldTable
PageObjects.datasetQuality.testSubjectSelectors.datasetQualityDetailsDegradedFieldTable
);
const rows =
await PageObjects.datasetQuality.getDatasetQualityFlyoutDegradedFieldTableRows();
await PageObjects.datasetQuality.getDatasetQualityDetailsDegradedFieldTableRows();
expect(rows.length).to.eql(2);
await PageObjects.datasetQuality.closeFlyout();
});
it('should display Spark Plot for every row of degraded fields', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: degradedDataStreamName,
});
const rows =
await PageObjects.datasetQuality.getDatasetQualityFlyoutDegradedFieldTableRows();
await PageObjects.datasetQuality.getDatasetQualityDetailsDegradedFieldTableRows();
const sparkPlots = await testSubjects.findAll(
PageObjects.datasetQuality.testSubjectSelectors.datasetQualitySparkPlot
);
expect(rows.length).to.be(sparkPlots.length);
await PageObjects.datasetQuality.closeFlyout();
});
it('should sort the table when the count table header is clicked', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: degradedDataStreamName,
});
const table = await PageObjects.datasetQuality.parseDegradedFieldTable();
@ -379,12 +407,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const sortedCellTexts = await countColumn.getCellTexts();
expect(cellTexts.reverse()).to.eql(sortedCellTexts);
await PageObjects.datasetQuality.closeFlyout();
});
it('should update the URL when the table is sorted', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: degradedDataStreamName,
});
const table = await PageObjects.datasetQuality.parseDegradedFieldTable();
const countColumn = table['Docs count'];
@ -410,8 +438,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
'sort:(direction:asc,field:count)'
);
});
await PageObjects.datasetQuality.closeFlyout();
});
// This is the only test which ingest data during the test.
@ -419,7 +445,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
// Even though this test ingest data, it can also be freely moved inside
// this describe block, and it won't affect any of the existing tests
it('should update the table when new data is ingested and the flyout is refreshed using the time selector', async () => {
await PageObjects.datasetQuality.openDatasetFlyout(degradedDatasetName);
await PageObjects.datasetQuality.navigateToDetails({
dataStream: degradedDataStreamName,
});
const table = await PageObjects.datasetQuality.parseDegradedFieldTable();
@ -434,7 +462,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
}),
]);
await PageObjects.datasetQuality.refreshFlyout();
await PageObjects.datasetQuality.refreshDetailsPageData();
const updatedTable = await PageObjects.datasetQuality.parseDegradedFieldTable();
const updatedCountColumn = updatedTable['Docs count'];
@ -445,8 +473,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const singleValueNow = parseInt(updatedCellTexts[0], 10);
expect(singleValueNow).to.be.greaterThan(singleValuePreviously);
await PageObjects.datasetQuality.closeFlyout();
});
});
});

View file

@ -13,7 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./dataset_quality_summary'));
loadTestFile(require.resolve('./dataset_quality_table'));
loadTestFile(require.resolve('./dataset_quality_table_filters'));
loadTestFile(require.resolve('./dataset_quality_flyout'));
loadTestFile(require.resolve('./dataset_quality_privileges'));
loadTestFile(require.resolve('./dataset_quality_details'));
});
}