[Dataset quality] Added dataQuality locator (#184588)

Closes https://github.com/elastic/kibana/issues/183406

## 📝  Summary
This PR adds a basic locator to dataQuality plugin, where timeRange
filters can be shared

## 🎥 Demo


47dabe6f-fe89-4075-8688-1e53332cdd9a

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Yngrid Coello 2024-05-31 19:50:09 +02:00 committed by GitHub
parent 863c4f3d04
commit 9a6dacb697
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 364 additions and 1 deletions

View file

@ -13,3 +13,6 @@ export const PLUGIN_NAME = i18n.translate('xpack.dataQuality.name', {
});
export { DATA_QUALITY_URL_STATE_KEY, datasetQualityUrlSchemaV1 } from './url_schema';
export { DATA_QUALITY_LOCATOR_ID } from './locators';
export type { DataQualityLocatorParams } from './locators';

View file

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

View file

@ -0,0 +1,33 @@
/*
* 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, LocatorPublic } from '@kbn/share-plugin/public';
import {
DataQualityLocatorDependencies,
DataQualityLocatorParams,
DATA_QUALITY_LOCATOR_ID,
} from './types';
import { constructDatasetQualityLocatorPath } from './construct_dataset_quality_locator_path';
export type DatasetQualityLocator = LocatorPublic<DataQualityLocatorParams>;
export class DatasetQualityLocatorDefinition
implements LocatorDefinition<DataQualityLocatorParams>
{
public readonly id = DATA_QUALITY_LOCATOR_ID;
constructor(protected readonly deps: DataQualityLocatorDependencies) {}
public readonly getLocation = async (params: DataQualityLocatorParams) => {
const { useHash, managementLocator } = this.deps;
return await constructDatasetQualityLocatorPath({
useHash,
managementLocator,
locatorParams: params,
});
};
}

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DatasetQualityLocator } from './dataset_quality_locator';
export * from './dataset_quality_locator';
export * from './types';
export interface DataQualityLocators {
datasetQualityLocator: DatasetQualityLocator;
}

View file

@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { DatasetQualityLocatorDefinition } from './dataset_quality_locator';
import { DataQualityLocatorDependencies } from './types';
const createMockLocator = (id: string, section: string) => ({
id,
navigate: jest.fn(),
getRedirectUrl: jest.fn(),
getLocation: jest.fn().mockReturnValue({ app: id, path: `/${section}`, state: {} }),
getUrl: jest.fn(),
navigateSync: jest.fn(),
useUrl: jest.fn(),
telemetry: jest.fn(),
inject: jest.fn(),
extract: jest.fn(),
migrations: jest.fn(),
});
const setup = async () => {
const dep: DataQualityLocatorDependencies = {
useHash: false,
managementLocator: createMockLocator('management', 'data/data_quality'),
};
const datasetQualityLocator = new DatasetQualityLocatorDefinition(dep);
return {
datasetQualityLocator,
};
};
describe('Data quality Locators', () => {
const timeRange = { to: 'now', from: 'now-30m' };
describe('Dataset Quality Locator', () => {
it('should create a link with correct path and no state', async () => {
const { datasetQualityLocator } = await setup();
const location = await datasetQualityLocator.getLocation({});
expect(location).toMatchObject({
app: '',
path: 'management/data/data_quality?pageState=(v:1)',
state: {},
});
});
it('should create a link with correct timeRange', async () => {
const refresh = {
pause: false,
value: 0,
};
const locatorParams = {
filters: {
timeRange: {
...timeRange,
refresh,
},
},
};
const { datasetQualityLocator } = await setup();
const location = await datasetQualityLocator.getLocation(locatorParams);
expect(location).toMatchObject({
app: '',
path: 'management/data/data_quality?pageState=(filters:(timeRange:(from:now-30m,refresh:(pause:!f,value:0),to:now)),v:1)',
state: {},
});
});
});
});

View file

@ -0,0 +1,39 @@
/*
* 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 { ManagementAppLocatorParams } from '@kbn/management-plugin/common/locator';
import { LocatorPublic } from '@kbn/share-plugin/common';
import { SerializableRecord } from '@kbn/utility-types';
export const DATA_QUALITY_LOCATOR_ID = 'DATA_QUALITY_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 Filters = {
timeRange: TimeRangeConfig;
};
export interface DataQualityLocatorParams extends SerializableRecord {
filters?: Filters;
}
export interface DataQualityLocatorDependencies {
useHash: boolean;
managementLocator: LocatorPublic<ManagementAppLocatorParams>;
}

View file

@ -0,0 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
module.exports = {
preset: '@kbn/test',
rootDir: '../../..',
roots: ['<rootDir>/x-pack/plugins/data_quality'],
coverageDirectory: '<rootDir>/target/kibana-coverage/jest/x-pack/plugins/data_quality',
coverageReporters: ['text', 'html'],
collectCoverageFrom: ['<rootDir>/x-pack/plugins/datas_quality/{common,public}/**/*.{ts,tsx}'],
};

View file

@ -11,6 +11,7 @@
"datasetQuality",
"management",
"features",
"share",
],
"optionalPlugins": [],
"requiredBundles": [

View file

@ -7,6 +7,8 @@
import { CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { ManagementAppMountParams } from '@kbn/management-plugin/public';
import { MANAGEMENT_APP_LOCATOR } from '@kbn/deeplinks-management/constants';
import { ManagementAppLocatorParams } from '@kbn/management-plugin/common/locator';
import {
DataQualityPluginSetup,
DataQualityPluginStart,
@ -14,6 +16,7 @@ import {
AppPluginSetupDependencies,
} from './types';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
import { DatasetQualityLocatorDefinition } from '../common/locators';
export class DataQualityPlugin
implements
@ -28,7 +31,8 @@ export class DataQualityPlugin
core: CoreSetup<AppPluginStartDependencies, DataQualityPluginStart>,
plugins: AppPluginSetupDependencies
): DataQualityPluginSetup {
const { management } = plugins;
const { management, share } = plugins;
const useHash = core.uiSettings.get('state:storeInSessionStorage');
management.sections.section.data.registerApp({
id: PLUGIN_ID,
@ -46,6 +50,18 @@ export class DataQualityPlugin
hideFromSidebar: true,
});
const managementLocator =
share.url.locators.get<ManagementAppLocatorParams>(MANAGEMENT_APP_LOCATOR);
if (managementLocator) {
share.url.locators.create(
new DatasetQualityLocatorDefinition({
useHash,
managementLocator,
})
);
}
return {};
}

View file

@ -7,6 +7,7 @@
import { DatasetQualityPluginStart } from '@kbn/dataset-quality-plugin/public';
import { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public';
import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface DataQualityPluginSetup {}
@ -16,9 +17,11 @@ export interface DataQualityPluginStart {}
export interface AppPluginSetupDependencies {
management: ManagementSetup;
share: SharePluginSetup;
}
export interface AppPluginStartDependencies {
datasetQuality: DatasetQualityPluginStart;
management: ManagementStart;
share: SharePluginStart;
}

View file

@ -24,6 +24,9 @@
"@kbn/i18n-react",
"@kbn/core-chrome-browser",
"@kbn/features-plugin",
"@kbn/share-plugin",
"@kbn/utility-types",
"@kbn/deeplinks-management",
],
"exclude": ["target/**/*"]
}

View file

@ -57,3 +57,10 @@ export const feedbackLinkTitle = i18n.translate(
export const createSLoLabel = i18n.translate('xpack.observabilityLogsExplorer.createSlo', {
defaultMessage: 'Create SLO',
});
export const datasetQualityLinkTitle = i18n.translate(
'xpack.observabilityLogsExplorer.datasetQualityLinkTitle',
{
defaultMessage: 'Datasets',
}
);

View file

@ -0,0 +1,98 @@
/*
* 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 { EuiHeaderLink } from '@elastic/eui';
import { LogsExplorerPublicState } from '@kbn/logs-explorer-plugin/public';
import { getRouterLinkProps } from '@kbn/router-utils';
import { BrowserUrlService } from '@kbn/share-plugin/public';
import { MatchedStateFromActor } from '@kbn/xstate-utils';
import { useActor } from '@xstate/react';
import React from 'react';
import { DataQualityLocatorParams, DATA_QUALITY_LOCATOR_ID } from '@kbn/data-quality-plugin/common';
import { datasetQualityLinkTitle } from '../../common/translations';
import {
ObservabilityLogsExplorerService,
useObservabilityLogsExplorerPageStateContext,
} from '../state_machines/observability_logs_explorer/src';
import { useKibanaContextForPlugin } from '../utils/use_kibana';
export const ConnectedDatasetQualityLink = React.memo(() => {
const {
services: {
share: { url },
},
} = useKibanaContextForPlugin();
const [pageState] = useActor(useObservabilityLogsExplorerPageStateContext());
if (pageState.matches({ initialized: 'validLogsExplorerState' })) {
return <DatasetQualityLink urlService={url} pageState={pageState} />;
} else {
return <DatasetQualityLink urlService={url} />;
}
});
type InitializedPageState = MatchedStateFromActor<
ObservabilityLogsExplorerService,
{ initialized: 'validLogsExplorerState' }
>;
const constructLocatorParams = (
logsExplorerState: LogsExplorerPublicState
): DataQualityLocatorParams => {
const { time, refreshInterval } = logsExplorerState;
const locatorParams: DataQualityLocatorParams = {
filters: {
timeRange: {
from: time?.from || 'now-24h',
to: time?.to || 'now',
refresh: {
pause: refreshInterval ? refreshInterval.pause : false,
value: refreshInterval ? refreshInterval.value : 60000,
},
},
},
};
return locatorParams;
};
export const DatasetQualityLink = React.memo(
({
urlService,
pageState,
}: {
urlService: BrowserUrlService;
pageState?: InitializedPageState;
}) => {
const locator = urlService.locators.get<DataQualityLocatorParams>(DATA_QUALITY_LOCATOR_ID);
const locatorParams: DataQualityLocatorParams = pageState
? constructLocatorParams(pageState.context.logsExplorerState)
: {};
const datasetQualityUrl = locator?.getRedirectUrl(locatorParams);
const navigateToDatasetQuality = () => {
locator?.navigate(locatorParams);
};
const datasetQualityLinkProps = getRouterLinkProps({
href: datasetQualityUrl,
onClick: navigateToDatasetQuality,
});
return (
<EuiHeaderLink
{...datasetQualityLinkProps}
color="primary"
data-test-subj="logsExplorerDatasetQualityLink"
>
{datasetQualityLinkTitle}
</EuiHeaderLink>
);
}
);

View file

@ -50,6 +50,7 @@
"@kbn/analytics-client",
"@kbn/core-analytics-browser",
"@kbn/react-hooks",
"@kbn/data-quality-plugin",
],
"exclude": [
"target/**/*"