mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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:
parent
32dd3730fa
commit
8431033910
83 changed files with 1178 additions and 4293 deletions
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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: {},
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -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 />;
|
||||
}
|
|
@ -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>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
];
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
`;
|
|
@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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."
|
||||
/>
|
||||
);
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -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 ? '+' : ''}`;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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, {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
} & {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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 };
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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(() => {
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -52,9 +52,7 @@ export class DatasetQualityPlugin
|
|||
|
||||
const createDatasetQualityController = createDatasetQualityControllerLazyFactory({
|
||||
core,
|
||||
plugins,
|
||||
dataStreamStatsClient,
|
||||
dataStreamDetailsClient,
|
||||
});
|
||||
|
||||
const DatasetQualityDetails = createDatasetQualityDetails({
|
||||
|
|
|
@ -34,6 +34,7 @@ export enum NavigationSource {
|
|||
Footer = 'footer',
|
||||
Summary = 'summary',
|
||||
Chart = 'chart',
|
||||
Trend = 'trend',
|
||||
Table = 'table',
|
||||
ActionMenu = 'action_menu',
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -14613,7 +14613,6 @@
|
|||
"xpack.datasetQuality.appTitle": "Qualité de l’ensemble 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 l’ensemble 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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue