Skip to content

Commit

Permalink
[Dataset quality] Open in Log explorer action (#173272)
Browse files Browse the repository at this point in the history
Closes #170236.

### Changes

This PR focuses on adding an action per dataset + namespace to navigate
to Observability log explorer. At the same time I took the opportunity
to update `Open in discover` link to include the controls present in
`Observability Log Explorer` state (atm just `namespace`)

1. Extracted `getRouterLinkProps` to a new package for reusability.
2. New `Actions` column was added to table.
3. `LogExplorerLink ` component was introduced, to reuse the navigation
logic between the table and the flyout.
4. `getDiscoverFiltersFromState` was added to combine state filters and
controls into discover state when navigating to discover

#### Demo


https://github.com/elastic/kibana/assets/1313018/a3f38615-d8ae-432b-ba7b-05a6901f870c

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
yngrdyn and kibanamachine authored Dec 18, 2023
1 parent 0cccd5f commit ce293db
Show file tree
Hide file tree
Showing 25 changed files with 280 additions and 70 deletions.
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,7 @@ x-pack/test/plugin_functional/plugins/resolver_test @elastic/security-solution
examples/response_stream @elastic/ml-ui
packages/kbn-rison @elastic/kibana-operations
x-pack/plugins/rollup @elastic/platform-deployment-management
packages/kbn-router-utils @elastic/obs-ux-logs-team
examples/routing_example @elastic/kibana-core
packages/kbn-rrule @elastic/response-ops
packages/kbn-rule-data-utils @elastic/security-detections-response @elastic/response-ops @elastic/obs-ux-management-team
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@
"@kbn/response-stream-plugin": "link:examples/response_stream",
"@kbn/rison": "link:packages/kbn-rison",
"@kbn/rollup-plugin": "link:x-pack/plugins/rollup",
"@kbn/router-utils": "link:packages/kbn-router-utils",
"@kbn/routing-example-plugin": "link:examples/routing_example",
"@kbn/rrule": "link:packages/kbn-rrule",
"@kbn/rule-data-utils": "link:packages/kbn-rule-data-utils",
Expand Down
34 changes: 34 additions & 0 deletions packages/kbn-router-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# @kbn/router-utils

This package provides util functions when working with the router.

## getRouterLinkProps

Useful to generate link component properties for HTML elements, this link properties will allow them to behave as native links and handle events such as open in a new tab, or client-side navigation without refreshing the whole page.

### Example

We want a button to both navigate to Discover client-side or open on a new window.

```ts
const DiscoverLink = (discoverLinkParams) => {
const discoverUrl = discover.locator?.getRedirectUrl(discoverLinkParams);

const navigateToDiscover = () => {
discover.locator?.navigate(discoverLinkParams);
};

const linkProps = getRouterLinkProps({
href: discoverUrl,
onClick: navigateToDiscover,
});

return (
<>
<EuiButton {...linkProps}>
{discoverLinkTitle}
</EuiButton>
</>
);
};
```
9 changes: 9 additions & 0 deletions packages/kbn-router-utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

export { getRouterLinkProps } from './src/get_router_link_props';
13 changes: 13 additions & 0 deletions packages/kbn-router-utils/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 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.
*/

module.exports = {
preset: '@kbn/test',
rootDir: '../..',
roots: ['<rootDir>/packages/kbn-router-utils'],
};
5 changes: 5 additions & 0 deletions packages/kbn-router-utils/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"type": "shared-common",
"id": "@kbn/router-utils",
"owner": "@elastic/obs-ux-logs-team"
}
6 changes: 6 additions & 0 deletions packages/kbn-router-utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@kbn/router-utils",
"private": true,
"version": "1.0.0",
"license": "SSPL-1.0 OR Elastic License 2.0"
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/*
* 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.
* 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.
*/

interface GetRouterLinkPropsDeps {
Expand All @@ -15,6 +16,18 @@ const isModifiedEvent = (event: React.MouseEvent<HTMLAnchorElement>) =>

const isLeftClickEvent = (event: React.MouseEvent<HTMLAnchorElement>) => event.button === 0;

/**
*
* getRouterLinkProps is an util that enable HTML elements, such buttons, to
* behave as links.
* @example
* const linkProps = getRouterLinkProps({ href: 'https://my-link', onClick: () => {console.log('click event')} });
* <EuiButton {...linkProps}>My custom link</EuiButton>
* @param href target url
* @param onClick onClick callback
* @returns An object that contains an href and a guardedClick handler that will
* manage behaviours such as leftClickEvent and event with modifiers (Ctrl, Shift, etc)
*/
export const getRouterLinkProps = ({ href, onClick }: GetRouterLinkPropsDeps) => {
const guardedClickHandler = (event: React.MouseEvent<HTMLAnchorElement>) => {
if (event.defaultPrevented) {
Expand Down
19 changes: 19 additions & 0 deletions packages/kbn-router-utils/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"types": [
"jest",
"node",
"react"
]
},
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"target/**/*"
],
"kbn_references": []
}
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,8 @@
"@kbn/rison/*": ["packages/kbn-rison/*"],
"@kbn/rollup-plugin": ["x-pack/plugins/rollup"],
"@kbn/rollup-plugin/*": ["x-pack/plugins/rollup/*"],
"@kbn/router-utils": ["packages/kbn-router-utils"],
"@kbn/router-utils/*": ["packages/kbn-router-utils/*"],
"@kbn/routing-example-plugin": ["examples/routing_example"],
"@kbn/routing-example-plugin/*": ["examples/routing_example/*"],
"@kbn/rrule": ["packages/kbn-rrule"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Integration } from './integration';
import { DataStreamStatType, IntegrationType } from './types';

export class DataStreamStat {
rawName: string;
name: DataStreamStatType['name'];
namespace: string;
title: string;
Expand All @@ -19,6 +20,7 @@ export class DataStreamStat {
degradedDocs?: number;

private constructor(dataStreamStat: DataStreamStat) {
this.rawName = dataStreamStat.name;
this.name = dataStreamStat.name;
this.title = dataStreamStat.title ?? dataStreamStat.name;
this.namespace = dataStreamStat.namespace;
Expand All @@ -33,7 +35,8 @@ export class DataStreamStat {
const [_type, dataset, namespace] = dataStreamStat.name.split('-');

const dataStreamStatProps = {
name: dataStreamStat.name,
rawName: dataStreamStat.name,
name: dataset,
title: dataStreamStat.integration?.datasets?.[dataset] ?? dataset,
namespace,
size: dataStreamStat.size,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
* 2.0.
*/

import React from 'react';
import {
EuiBadge,
EuiBasicTableColumn,
Expand All @@ -16,17 +15,19 @@ import {
EuiSkeletonRectangle,
EuiToolTip,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { PackageIcon } from '@kbn/fleet-plugin/public';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '@kbn/field-types';
import { PackageIcon } from '@kbn/fleet-plugin/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import React from 'react';
import {
DEGRADED_QUALITY_MINIMUM_PERCENTAGE,
POOR_QUALITY_MINIMUM_PERCENTAGE,
} from '../../../common/constants';
import { DataStreamStat } from '../../../common/data_streams_stats/data_stream_stat';
import loggingIcon from '../../icons/logging.svg';
import { LogExplorerLink } from '../log_explorer_link';
import { QualityIndicator, QualityPercentageIndicator } from '../quality_indicator';

const nameColumnName = i18n.translate('xpack.datasetQuality.nameColumnName', {
Expand All @@ -45,6 +46,17 @@ const degradedDocsColumnName = i18n.translate('xpack.datasetQuality.degradedDocs
defaultMessage: 'Degraded Docs',
});

const lastActivityColumnName = i18n.translate('xpack.datasetQuality.lastActivityColumnName', {
defaultMessage: 'Last Activity',
});

const actionsColumnName = i18n.translate('xpack.datasetQuality.actionsColumnName', {
defaultMessage: 'Actions',
});
const openActionName = i18n.translate('xpack.datasetQuality.openActionName', {
defaultMessage: 'Open',
});

const degradedDocsDescription = (minimimPercentage: number) =>
i18n.translate('xpack.datasetQuality.degradedDocsQualityDescription', {
defaultMessage: 'greater than {minimimPercentage}%',
Expand Down Expand Up @@ -84,10 +96,6 @@ const degradedDocsColumnTooltip = (
/>
);

const lastActivityColumnName = i18n.translate('xpack.datasetQuality.lastActivityColumnName', {
defaultMessage: 'Last Activity',
});

export const getDatasetQualitTableColumns = ({
fieldFormats,
loadingDegradedStats,
Expand Down Expand Up @@ -170,5 +178,12 @@ export const getDatasetQualitTableColumns = ({
.convert(timestamp),
sortable: true,
},
{
name: actionsColumnName,
render: (dataStreamStat: DataStreamStat) => (
<LogExplorerLink dataStreamStat={dataStreamStat} title={openActionName} />
),
width: '100px',
},
];
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { EuiLink } from '@elastic/eui';
import React from 'react';
import { getRouterLinkProps } from '@kbn/router-utils';
import {
SingleDatasetLocatorParams,
SINGLE_DATASET_LOCATOR_ID,
} from '@kbn/deeplinks-observability';
import { DataStreamStat } from '../../common/data_streams_stats/data_stream_stat';
import { useKibanaContextForPlugin } from '../utils';

export const LogExplorerLink = React.memo(
({ dataStreamStat, title }: { dataStreamStat: DataStreamStat; title: string }) => {
const {
services: { share },
} = useKibanaContextForPlugin();
const params: SingleDatasetLocatorParams = {
dataset: dataStreamStat.name,
timeRange: {
from: 'now-1d',
to: 'now',
},
integration: dataStreamStat.integration?.name,
filterControls: {
namespace: {
mode: 'include',
values: [dataStreamStat.namespace],
},
},
};

const singleDatasetLocator =
share.url.locators.get<SingleDatasetLocatorParams>(SINGLE_DATASET_LOCATOR_ID);

const urlToLogExplorer = singleDatasetLocator?.getRedirectUrl(params);

const navigateToLogExplorer = () => {
singleDatasetLocator?.navigate(params) as Promise<void>;
};

const logExplorerLinkProps = getRouterLinkProps({
href: urlToLogExplorer,
onClick: navigateToLogExplorer,
});

return <EuiLink {...logExplorerLinkProps}>{title}</EuiLink>;
}
);
4 changes: 3 additions & 1 deletion x-pack/plugins/dataset_quality/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"@kbn/field-types",
"@kbn/io-ts-utils",
"@kbn/observability-plugin",
"@kbn/es-types"
"@kbn/es-types",
"@kbn/deeplinks-observability",
"@kbn/router-utils",
],
"exclude": [
"target/**/*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { EuiBadge, EuiButton, EuiHorizontalRule } from '@elastic/eui';
import React from 'react';
import { getRouterLinkProps } from '../../../utils/get_router_link_props';
import { getRouterLinkProps } from '@kbn/router-utils';
import { DiscoverEsqlUrlProps } from '../../../hooks/use_esql';
import { technicalPreview, tryEsql } from '../constants';

Expand Down
10 changes: 3 additions & 7 deletions x-pack/plugins/log_explorer/public/controller/public_state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ import {
DEFAULT_CONTEXT,
LogExplorerControllerContext,
} from '../state_machines/log_explorer_controller';
import {
LogExplorerPublicState,
LogExplorerPublicStateUpdate,
OptionsListControlOption,
} from './types';
import { LogExplorerPublicState, LogExplorerPublicStateUpdate, OptionsListControl } from './types';

export const getPublicStateFromContext = (
context: LogExplorerControllerContext
Expand Down Expand Up @@ -80,7 +76,7 @@ const getPublicControlsStateFromControlPanels = (

const getOptionsListPublicControlStateFromControlPanel = (
optionsListControlPanel: ControlPanels[string]
): OptionsListControlOption => ({
): OptionsListControl => ({
mode: optionsListControlPanel.explicitInput.exclude ? 'exclude' : 'include',
selection: optionsListControlPanel.explicitInput.existsSelected
? { type: 'exists' }
Expand Down Expand Up @@ -113,7 +109,7 @@ const getControlPanelsFromPublicControlsState = (

const getControlPanelFromOptionsListPublicControlState = (
controlId: string,
publicControlState: OptionsListControlOption
publicControlState: OptionsListControl
): ControlPanels[string] => {
const defaultControlPanelConfig = controlPanelConfigs[controlId];

Expand Down
20 changes: 11 additions & 9 deletions x-pack/plugins/log_explorer/public/controller/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,21 @@ export type LogExplorerDiscoverServices = Pick<
};

export interface OptionsListControlOption {
type: 'options';
selectedOptions: string[];
}

export interface OptionsListControlExists {
type: 'exists';
}

export interface OptionsListControl {
mode: 'include' | 'exclude';
selection:
| {
type: 'options';
selectedOptions: string[];
}
| {
type: 'exists';
};
selection: OptionsListControlOption | OptionsListControlExists;
}

export interface ControlOptions {
[availableControlsPanels.NAMESPACE]?: OptionsListControlOption;
[availableControlsPanels.NAMESPACE]?: OptionsListControl;
}

// we might want to wrap this into an object that has a "state value" laster
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/log_explorer/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type { LogExplorerPluginSetup, LogExplorerPluginStart } from './types';
export {
getDiscoverColumnsFromDisplayOptions,
getDiscoverGridFromDisplayOptions,
getDiscoverFiltersFromState,
} from './utils/convert_discover_app_state';

export function plugin(context: PluginInitializerContext<LogExplorerConfig>) {
Expand Down
Loading

0 comments on commit ce293db

Please sign in to comment.