Skip to content

Commit

Permalink
[Dataset Quality] Replication of dataset flyout as an independent com…
Browse files Browse the repository at this point in the history
…ponent (elastic#189532)

## Summary

Relates to - elastic#184572
Figma Design -
https://www.figma.com/design/8WVWLeVn8mvoUm0VGgbSbB/Data-set-quality-V2?node-id=3564-73485&t=KADTdNFiiOBJ7rOS-0

**NOTE: This PR is part of a multi series PRs. Hence expect it to not do
everything.**

### What are we going to do?

1. The content of the flyout, henceforth will be known as Dataset
Quality Details is being copied to a component with the same name.
2. This component can be initialised as page, like in Management app or
used as an individual component in a flyout in Unified Doc Viewer for
example. As scope of this PR, a page in Management app has been created
which will load this detailed component. A new route will be created
with breadcrumb.
3. This page will co-live with the Flyout for now, accessible only via
direct URL. In subsequent PR, when we remove the Flyout completely, we
will change the action in Dataset Quality to instead of opening a Flyout
to navigating to this new page.

### What's in this PR ?

1. As part of this change, i have created a complete new State Machine,
Controller for Dataset Quality Details component which is responsible
for replicating the Flyout.
2. A dedicated route registered under `/details` where this component
will live at the moment. Sample URL will look like this

`http://localhost:5601/pfd/app/management/data/data_quality/details?pageState=(dataStream:logs-synth.1-default,v:1)`
3. The individual components which currently load inside the flyout may
be duplicated for time being.
4. Validation when no data stream provided.
5. Breadcrumb for the Management page

### What's not in this PR

1. Tests needs to be migrated, they will be done as part of the Next PR
2. Telemetry for Flyout has been removed. It will be added as part of
next PR.
3. Existing Flyout code has not be removed. That needs to be removed and
the old state machine needs to be meticulously cleaned.
6. Swapping the Click to Open Flyout to Page needs to be done when the
above 3 are ready.

## Screenshot

### Good scenario

<img width="1482" alt="image"
src="https://github.com/user-attachments/assets/4409eb57-89d5-477c-a946-1b7a45df074c">

### When datastream does not exist

<img width="1527" alt="image"
src="https://github.com/user-attachments/assets/66d735aa-8f0f-4fb8-b57c-4d22cecad2c7">

### When invalid state is provided by the page, it redirects to parent
Dataset Quality Page

![Aug-09-2024
13-55-54](https://github.com/user-attachments/assets/ea8379c5-0642-458c-8164-f50a17818895)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
achyutjhunjhunwala and kibanamachine authored Aug 14, 2024
1 parent c026279 commit 2a33601
Show file tree
Hide file tree
Showing 110 changed files with 3,960 additions and 477 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pageLoadAssetSize:
dashboardEnhanced: 65646
data: 454087
dataQuality: 19384
datasetQuality: 50624
datasetQuality: 52000
dataViewEditor: 28082
dataViewFieldEditor: 42021
dataViewManagement: 5300
Expand Down
46 changes: 46 additions & 0 deletions x-pack/plugins/data_quality/common/url_schema/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,50 @@
* 2.0.
*/

import * as rt from 'io-ts';

export const DATA_QUALITY_URL_STATE_KEY = 'pageState';

export const directionRT = rt.keyof({
asc: null,
desc: null,
});

export const sortRT = rt.strict({
field: rt.string,
direction: directionRT,
});

export const tableRT = rt.exact(
rt.partial({
page: rt.number,
rowsPerPage: rt.number,
sort: sortRT,
})
);

export const timeRangeRT = rt.strict({
from: rt.string,
to: rt.string,
refresh: rt.strict({
pause: rt.boolean,
value: rt.number,
}),
});

export const degradedFieldRT = rt.exact(
rt.partial({
table: tableRT,
})
);

export const dataStreamRT = new rt.Type<string, string, unknown>(
'dataStreamRT',
(input: unknown): input is string =>
typeof input === 'string' && (input.match(/-/g) || []).length === 2,
(input, context) =>
typeof input === 'string' && (input.match(/-/g) || []).length === 2
? rt.success(input)
: rt.failure(input, context),
rt.identity
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import * as rt from 'io-ts';
import { dataStreamRT, degradedFieldRT, timeRangeRT } from './common';

export const urlSchemaRT = rt.exact(
rt.intersection([
rt.type({
dataStream: dataStreamRT,
}),
rt.partial({
v: rt.literal(1),
timeRange: timeRangeRT,
breakdownField: rt.string,
degradedFields: degradedFieldRT,
}),
])
);

export type UrlSchema = rt.TypeOf<typeof urlSchemaRT>;
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,7 @@
*/

import * as rt from 'io-ts';

export const directionRT = rt.keyof({
asc: null,
desc: null,
});

export const sortRT = rt.strict({
field: rt.string,
direction: directionRT,
});

export const tableRT = rt.exact(
rt.partial({
page: rt.number,
rowsPerPage: rt.number,
sort: sortRT,
})
);
import { degradedFieldRT, tableRT, timeRangeRT } from './common';

const integrationRT = rt.strict({
name: rt.string,
Expand All @@ -46,21 +29,6 @@ const datasetRT = rt.intersection([
),
]);

const timeRangeRT = rt.strict({
from: rt.string,
to: rt.string,
refresh: rt.strict({
pause: rt.boolean,
value: rt.number,
}),
});

const degradedFieldRT = rt.exact(
rt.partial({
table: tableRT,
})
);

export const flyoutRT = rt.exact(
rt.partial({
dataset: datasetRT,
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/data_quality/common/url_schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
*/

export { DATA_QUALITY_URL_STATE_KEY } from './common';
export * as datasetQualityUrlSchemaV1 from './url_schema_v1';
export * as datasetQualityUrlSchemaV1 from './dataset_quality_url_schema_v1';
export * as datasetQualityDetailsUrlSchemaV1 from './dataset_quality_detils_url_schema_v1';
3 changes: 2 additions & 1 deletion x-pack/plugins/data_quality/public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { KbnUrlStateStorageFromRouterProvider } from './utils/kbn_url_state_context';
import { useKibanaContextForPluginProvider } from './utils/use_kibana';
import { AppPluginStartDependencies, DataQualityPluginStart } from './types';
import { DatasetQualityRoute } from './routes';
import { DatasetQualityRoute, DatasetQualityDetailsRoute } from './routes';
import { PLUGIN_ID } from '../common';

export const renderApp = (
Expand Down Expand Up @@ -55,6 +55,7 @@ const AppWithExecutionContext = ({
<PerformanceContextProvider>
<Routes>
<Route path="/" exact={true} render={() => <DatasetQualityRoute />} />
<Route path="/details" exact={true} render={() => <DatasetQualityDetailsRoute />} />
</Routes>
</PerformanceContextProvider>
</Router>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { IToasts } from '@kbn/core-notifications-browser';
import { DatasetQualityPluginStart } from '@kbn/dataset-quality-plugin/public';
import { DatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller';
import { DatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality';
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import React, { createContext, useContext, useEffect, useState } from 'react';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
import { DatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller';
import type { DatasetQualityController } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { PLUGIN_NAME } from '../../../common';
Expand All @@ -21,7 +21,7 @@ export const DatasetQualityRoute = () => {
services: { chrome, datasetQuality, notifications, appParams },
} = useKibanaContextForPlugin();

useBreadcrumbs(PLUGIN_NAME, appParams, chrome);
useBreadcrumbs([{ text: PLUGIN_NAME }], appParams, chrome);

return (
<DatasetQualityContextProvider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import {
DatasetQualityFlyoutOptions,
DatasetQualityPublicStateUpdate,
} from '@kbn/dataset-quality-plugin/public/controller';
} 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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { IToasts } from '@kbn/core-notifications-browser';
import { DatasetQualityPublicState } from '@kbn/dataset-quality-plugin/public/controller';
import { DatasetQualityPublicState } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality';
import { createPlainError, formatErrors } from '@kbn/io-ts-utils';
import { IKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
import * as Either from 'fp-ts/lib/Either';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { IToasts } from '@kbn/core-notifications-browser';
import { DatasetQualityPluginStart } from '@kbn/dataset-quality-plugin/public';
import { DatasetQualityDetailsController } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality_details';
import { IKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import type { ChromeBreadcrumb } from '@kbn/core-chrome-browser';
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
import { getBreadcrumbValue, useBreadcrumbs } from '../../utils/use_breadcrumbs';
import {
getDatasetQualityDetailsStateFromUrl,
updateUrlFromDatasetQualityDetailsState,
} from './url_state_storage_service';
import { PLUGIN_ID, PLUGIN_NAME } from '../../../common';

const DatasetQualityDetailsContext = createContext<{
controller?: DatasetQualityDetailsController;
}>({});

interface ContextProps {
children: JSX.Element;
urlStateStorageContainer: IKbnUrlStateStorage;
toastsService: IToasts;
datasetQuality: DatasetQualityPluginStart;
}

export function DatasetQualityDetailsContextProvider({
children,
urlStateStorageContainer,
toastsService,
datasetQuality,
}: ContextProps) {
const [controller, setController] = useState<DatasetQualityDetailsController>();
const history = useHistory();
const {
services: {
chrome,
appParams,
application: { navigateToApp },
},
} = useKibanaContextForPlugin();
const rootBreadCrumb = useMemo(
() => ({
text: PLUGIN_NAME,
onClick: () => navigateToApp('management', { path: `/data/${PLUGIN_ID}` }),
}),
[navigateToApp]
);
const [breadcrumbs, setBreadcrumbs] = useState<ChromeBreadcrumb[]>([rootBreadCrumb]);

useEffect(() => {
async function getDatasetQualityDetailsController() {
const initialState = getDatasetQualityDetailsStateFromUrl({
urlStateStorageContainer,
toastsService,
});

// state initialization is under progress
if (initialState === undefined) {
return;
}

// state initialized but empty
if (initialState === null) {
history.push('/');
return;
}

const datasetQualityDetailsController =
await datasetQuality.createDatasetQualityDetailsController({
initialState,
});
datasetQualityDetailsController.service.start();

setController(datasetQualityDetailsController);

const datasetQualityStateSubscription = datasetQualityDetailsController.state$.subscribe(
(state) => {
updateUrlFromDatasetQualityDetailsState({
urlStateStorageContainer,
datasetQualityDetailsState: state,
});
const breadcrumbValue = getBreadcrumbValue(state.dataStream, state.integration);
setBreadcrumbs([rootBreadCrumb, { text: breadcrumbValue }]);
}
);

return () => {
datasetQualityDetailsController.service.stop();
datasetQualityStateSubscription.unsubscribe();
};
}

getDatasetQualityDetailsController();
}, [datasetQuality, history, rootBreadCrumb, toastsService, urlStateStorageContainer]);

useBreadcrumbs(breadcrumbs, appParams, chrome);

return (
<DatasetQualityDetailsContext.Provider value={{ controller }}>
{children}
</DatasetQualityDetailsContext.Provider>
);
}

export const useDatasetQualityDetailsContext = () => {
const context = useContext(DatasetQualityDetailsContext);
if (context === undefined) {
throw new Error(
'useDatasetQualityDetailContext must be used within a <DatasetQualityDetailsContextProvider />'
);
}
return context;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui';
import type { DatasetQualityDetailsController } from '@kbn/dataset-quality-plugin/public/controller/dataset_quality_details';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKbnUrlStateStorageFromRouterContext } from '../../utils/kbn_url_state_context';
import { useKibanaContextForPlugin } from '../../utils/use_kibana';
import { DatasetQualityDetailsContextProvider, useDatasetQualityDetailsContext } from './context';

export const DatasetQualityDetailsRoute = () => {
const urlStateStorageContainer = useKbnUrlStateStorageFromRouterContext();
const {
services: { datasetQuality, notifications },
} = useKibanaContextForPlugin();

return (
<DatasetQualityDetailsContextProvider
datasetQuality={datasetQuality}
urlStateStorageContainer={urlStateStorageContainer}
toastsService={notifications.toasts}
>
<ConnectedContent />
</DatasetQualityDetailsContextProvider>
);
};

const ConnectedContent = React.memo(() => {
const { controller } = useDatasetQualityDetailsContext();

return controller ? (
<InitializedContent datasetQualityDetailsController={controller} />
) : (
<>
<EuiEmptyPrompt
icon={<EuiLoadingLogo logo="logoKibana" size="xl" />}
title={
<FormattedMessage
id="xpack.dataQuality.details.Initializing"
defaultMessage="Initializing Data set quality details page"
/>
}
/>
</>
);
});

const InitializedContent = React.memo(
({
datasetQualityDetailsController,
}: {
datasetQualityDetailsController: DatasetQualityDetailsController;
}) => {
const {
services: { datasetQuality },
} = useKibanaContextForPlugin();

return <datasetQuality.DatasetQualityDetails controller={datasetQualityDetailsController} />;
}
);
Loading

0 comments on commit 2a33601

Please sign in to comment.