[ML] Add recognized modules links for Index data visualizer (#131342)

* [ML] Add dynamic registration of links for both index and file

* [ML] Consolidate type imports

* [ML] Revert uptime changes

* [ML] Fix cards visible when canDisplay is false

* [ML] Shorten create job text

* [ML] Remove as assertions

* [ML] Rename to GetAdditionalLinks

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Quynh Nguyen 2022-05-04 13:21:51 -05:00 committed by GitHub
parent fe76adbc3a
commit a632484214
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 277 additions and 165 deletions

View file

@ -5,7 +5,7 @@
* 2.0. * 2.0.
*/ */
import React, { FC, ReactElement } from 'react'; import React, { FC } from 'react';
import { import {
EuiIcon, EuiIcon,
@ -18,8 +18,8 @@ import {
EuiLink, EuiLink,
} from '@elastic/eui'; } from '@elastic/eui';
interface Props { export interface LinkCardProps {
icon: IconType | ReactElement; icon: IconType;
iconAreaLabel?: string; iconAreaLabel?: string;
title: any; title: any;
description: any; description: any;
@ -31,7 +31,7 @@ interface Props {
// Component for rendering a card which links to the Create Job page, displaying an // Component for rendering a card which links to the Create Job page, displaying an
// icon, card title, description and link. // icon, card title, description and link.
export const LinkCard: FC<Props> = ({ export const LinkCard: FC<LinkCardProps> = ({
icon, icon,
iconAreaLabel, iconAreaLabel,
title, title,
@ -39,7 +39,7 @@ export const LinkCard: FC<Props> = ({
onClick, onClick,
href, href,
isDisabled, isDisabled,
'data-test-subj': dateTestSubj, 'data-test-subj': dataTestSubj,
}) => { }) => {
const linkHrefAndOnClickProps = { const linkHrefAndOnClickProps = {
...(href ? { href } : {}), ...(href ? { href } : {}),
@ -58,7 +58,7 @@ export const LinkCard: FC<Props> = ({
background: 'transparent', background: 'transparent',
outline: 'none', outline: 'none',
}} }}
data-test-subj={dateTestSubj} data-test-subj={dataTestSubj}
color="subdued" color="subdued"
{...linkHrefAndOnClickProps} {...linkHrefAndOnClickProps}
> >

View file

@ -5,5 +5,5 @@
* 2.0. * 2.0.
*/ */
export type { ResultLink } from './results_links'; export type { ResultLink, GetAdditionalLinks, GetAdditionalLinksParams } from './results_links';
export { ResultsLinks } from './results_links'; export { ResultsLinks } from './results_links';

View file

@ -12,10 +12,23 @@ import { EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui';
import { TimeRange, RefreshInterval } from '@kbn/data-plugin/public'; import { TimeRange, RefreshInterval } from '@kbn/data-plugin/public';
import { FindFileStructureResponse } from '@kbn/file-upload-plugin/common'; import { FindFileStructureResponse } from '@kbn/file-upload-plugin/common';
import type { FileUploadPluginStart } from '@kbn/file-upload-plugin/public'; import type { FileUploadPluginStart } from '@kbn/file-upload-plugin/public';
import { flatten } from 'lodash';
import { LinkCardProps } from '../link_card/link_card';
import { useDataVisualizerKibana } from '../../../kibana_context'; import { useDataVisualizerKibana } from '../../../kibana_context';
import { isDefined } from '../../util/is_defined';
type LinkType = 'file' | 'index'; type LinkType = 'file' | 'index';
export interface GetAdditionalLinksParams {
dataViewId: string;
dataViewTitle?: string;
globalState?: any;
}
export type GetAdditionalLinks = Array<
(params: GetAdditionalLinksParams) => Promise<ResultLink[] | undefined>
>;
export interface ResultLink { export interface ResultLink {
id: string; id: string;
type: LinkType; type: LinkType;
@ -24,7 +37,7 @@ export interface ResultLink {
description: string; description: string;
getUrl(params?: any): Promise<string>; getUrl(params?: any): Promise<string>;
canDisplay(params?: any): Promise<boolean>; canDisplay(params?: any): Promise<boolean>;
dataTestSubj?: string; 'data-test-subj'?: string;
} }
interface Props { interface Props {
@ -34,7 +47,7 @@ interface Props {
timeFieldName?: string; timeFieldName?: string;
createDataView: boolean; createDataView: boolean;
showFilebeatFlyout(): void; showFilebeatFlyout(): void;
additionalLinks: ResultLink[]; getAdditionalLinks?: GetAdditionalLinks;
} }
interface GlobalState { interface GlobalState {
@ -51,7 +64,7 @@ export const ResultsLinks: FC<Props> = ({
timeFieldName, timeFieldName,
createDataView, createDataView,
showFilebeatFlyout, showFilebeatFlyout,
additionalLinks, getAdditionalLinks,
}) => { }) => {
const { const {
services: { services: {
@ -70,7 +83,7 @@ export const ResultsLinks: FC<Props> = ({
const [discoverLink, setDiscoverLink] = useState(''); const [discoverLink, setDiscoverLink] = useState('');
const [indexManagementLink, setIndexManagementLink] = useState(''); const [indexManagementLink, setIndexManagementLink] = useState('');
const [dataViewsManagementLink, setDataViewsManagementLink] = useState(''); const [dataViewsManagementLink, setDataViewsManagementLink] = useState('');
const [generatedLinks, setGeneratedLinks] = useState<Record<string, string>>({}); const [asyncHrefCards, setAsyncHrefCards] = useState<LinkCardProps[]>();
useEffect(() => { useEffect(() => {
let unmounted = false; let unmounted = false;
@ -93,22 +106,30 @@ export const ResultsLinks: FC<Props> = ({
getDiscoverUrl(); getDiscoverUrl();
Promise.all( if (Array.isArray(getAdditionalLinks)) {
additionalLinks.map(async ({ canDisplay, getUrl }) => { Promise.all(
if ((await canDisplay({ indexPatternId: dataViewId })) === false) { getAdditionalLinks.map(async (asyncCardGetter) => {
return null; const results = await asyncCardGetter({
} dataViewId,
return getUrl({ globalState, indexPatternId: dataViewId }); });
}) if (Array.isArray(results)) {
).then((urls) => { return await Promise.all(
const linksById = urls.reduce((acc, url, i) => { results.map(async (c) => ({
if (url !== null) { ...c,
acc[additionalLinks[i].id] = url; canDisplay: await c.canDisplay(),
} href: await c.getUrl(),
return acc; }))
}, {} as Record<string, string>); );
setGeneratedLinks(linksById); }
}); })
).then((cards) => {
setAsyncHrefCards(
flatten(cards)
.filter(isDefined)
.filter((d) => d.canDisplay === true)
);
});
}
if (!unmounted) { if (!unmounted) {
setIndexManagementLink( setIndexManagementLink(
@ -244,16 +265,15 @@ export const ResultsLinks: FC<Props> = ({
onClick={showFilebeatFlyout} onClick={showFilebeatFlyout}
/> />
</EuiFlexItem> </EuiFlexItem>
{additionalLinks {Array.isArray(asyncHrefCards) &&
.filter(({ id }) => generatedLinks[id] !== undefined) asyncHrefCards.map((link) => (
.map((link) => (
<EuiFlexItem> <EuiFlexItem>
<EuiCard <EuiCard
icon={<EuiIcon size="xxl" type={link.icon} />} icon={<EuiIcon size="xxl" type={link.icon} />}
data-test-subj="fileDataVisLink" data-test-subj="fileDataVisLink"
title={link.title} title={link.title}
description={link.description} description={link.description}
href={generatedLinks[link.id]} href={link.href}
/> />
</EuiFlexItem> </EuiFlexItem>
))} ))}

View file

@ -0,0 +1,10 @@
/*
* 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 function isDefined<T>(argument: T | undefined | null): argument is T {
return argument !== undefined && argument !== null;
}

View file

@ -369,7 +369,7 @@ export class FileDataVisualizerView extends Component {
hideBottomBar={this.hideBottomBar} hideBottomBar={this.hideBottomBar}
savedObjectsClient={this.savedObjectsClient} savedObjectsClient={this.savedObjectsClient}
fileUpload={this.props.fileUpload} fileUpload={this.props.fileUpload}
resultsLinks={this.props.resultsLinks} getAdditionalLinks={this.props.getAdditionalLinks}
capabilities={this.props.capabilities} capabilities={this.props.capabilities}
/> />

View file

@ -585,7 +585,7 @@ export class ImportView extends Component {
timeFieldName={timeFieldName} timeFieldName={timeFieldName}
createDataView={createDataView} createDataView={createDataView}
showFilebeatFlyout={this.showFilebeatFlyout} showFilebeatFlyout={this.showFilebeatFlyout}
additionalLinks={this.props.resultsLinks ?? []} getAdditionalLinks={this.props.getAdditionalLinks ?? []}
/> />
{isFilebeatFlyoutVisible && ( {isFilebeatFlyoutVisible && (

View file

@ -11,14 +11,14 @@ import { getCoreStart, getPluginsStart } from '../../kibana_services';
// @ts-ignore // @ts-ignore
import { FileDataVisualizerView } from './components/file_data_visualizer_view'; import { FileDataVisualizerView } from './components/file_data_visualizer_view';
import { ResultLink } from '../common/components/results_links'; import { GetAdditionalLinks } from '../common/components/results_links';
interface Props { interface Props {
additionalLinks?: ResultLink[]; getAdditionalLinks?: GetAdditionalLinks;
} }
export type FileDataVisualizerSpec = typeof FileDataVisualizer; export type FileDataVisualizerSpec = typeof FileDataVisualizer;
export const FileDataVisualizer: FC<Props> = ({ additionalLinks }) => { export const FileDataVisualizer: FC<Props> = ({ getAdditionalLinks }) => {
const coreStart = getCoreStart(); const coreStart = getCoreStart();
const { data, maps, embeddable, discover, share, security, fileUpload, cloud } = const { data, maps, embeddable, discover, share, security, fileUpload, cloud } =
getPluginsStart(); getPluginsStart();
@ -45,7 +45,7 @@ export const FileDataVisualizer: FC<Props> = ({ additionalLinks }) => {
savedObjectsClient={coreStart.savedObjects.client} savedObjectsClient={coreStart.savedObjects.client}
http={coreStart.http} http={coreStart.http}
fileUpload={fileUpload} fileUpload={fileUpload}
resultsLinks={additionalLinks} getAdditionalLinks={getAdditionalLinks}
capabilities={coreStart.application.capabilities} capabilities={coreStart.application.capabilities}
/> />
</CloudContext> </CloudContext>

View file

@ -11,28 +11,31 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiTitle } from '@elastic/eui'; import { EuiSpacer, EuiTitle } from '@elastic/eui';
import { DataView } from '@kbn/data-views-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public';
import { flatten } from 'lodash';
import { LinkCardProps } from '../../../common/components/link_card/link_card';
import { useDataVisualizerKibana } from '../../../kibana_context'; import { useDataVisualizerKibana } from '../../../kibana_context';
import { useUrlState } from '../../../common/util/url_state'; import { useUrlState } from '../../../common/util/url_state';
import { LinkCard } from '../../../common/components/link_card'; import { LinkCard } from '../../../common/components/link_card';
import { ResultLink } from '../../../common/components/results_links'; import { GetAdditionalLinks } from '../../../common/components/results_links';
import { isDefined } from '../../../common/util/is_defined';
interface Props { interface Props {
dataView: DataView; dataView: DataView;
searchString?: string | { [key: string]: any }; searchString?: string | { [key: string]: any };
searchQueryLanguage?: string; searchQueryLanguage?: string;
additionalLinks: ResultLink[]; getAdditionalLinks?: GetAdditionalLinks;
} }
export const ActionsPanel: FC<Props> = ({ export const ActionsPanel: FC<Props> = ({
dataView, dataView,
searchString, searchString,
searchQueryLanguage, searchQueryLanguage,
additionalLinks, getAdditionalLinks,
}) => { }) => {
const [globalState] = useUrlState('_g'); const [globalState] = useUrlState('_g');
const [discoverLink, setDiscoverLink] = useState(''); const [discoverLink, setDiscoverLink] = useState('');
const [generatedLinks, setGeneratedLinks] = useState<Record<string, string>>({}); const [asyncHrefCards, setAsyncHrefCards] = useState<LinkCardProps[]>();
const { const {
services: { services: {
@ -46,6 +49,7 @@ export const ActionsPanel: FC<Props> = ({
let unmounted = false; let unmounted = false;
const indexPatternId = dataView.id; const indexPatternId = dataView.id;
const indexPatternTitle = dataView.title;
const getDiscoverUrl = async (): Promise<void> => { const getDiscoverUrl = async (): Promise<void> => {
const isDiscoverAvailable = capabilities.discover?.show ?? false; const isDiscoverAvailable = capabilities.discover?.show ?? false;
if (!isDiscoverAvailable) return; if (!isDiscoverAvailable) return;
@ -68,24 +72,33 @@ export const ActionsPanel: FC<Props> = ({
setDiscoverLink(discoverUrl); setDiscoverLink(discoverUrl);
}; };
Promise.all( if (Array.isArray(getAdditionalLinks) && indexPatternId !== undefined) {
additionalLinks.map(async ({ canDisplay, getUrl }) => { Promise.all(
if ((await canDisplay({ indexPatternId })) === false) { getAdditionalLinks.map(async (asyncCardGetter) => {
return null; const results = await asyncCardGetter({
} dataViewId: indexPatternId,
return getUrl({ globalState, indexPatternId }); dataViewTitle: indexPatternTitle,
}) });
).then((urls) => { if (Array.isArray(results)) {
const linksById = urls.reduce((acc, url, i) => { return await Promise.all(
if (url !== null) { results.map(async (c) => ({
acc[additionalLinks[i].id] = url; ...c,
} canDisplay: await c.canDisplay(),
return acc; href: await c.getUrl(),
}, {} as Record<string, string>); }))
setGeneratedLinks(linksById); );
}); }
})
).then((cards) => {
setAsyncHrefCards(
flatten(cards)
.filter(isDefined)
.filter((d) => d.canDisplay === true)
);
});
}
getDiscoverUrl(); getDiscoverUrl();
return () => { return () => {
unmounted = true; unmounted = true;
}; };
@ -96,8 +109,8 @@ export const ActionsPanel: FC<Props> = ({
globalState, globalState,
capabilities, capabilities,
discover, discover,
additionalLinks,
data.query, data.query,
getAdditionalLinks,
]); ]);
// Note we use display:none for the DataRecognizer section as it needs to be // Note we use display:none for the DataRecognizer section as it needs to be
@ -105,20 +118,6 @@ export const ActionsPanel: FC<Props> = ({
// controls whether the recognizer section is ultimately displayed. // controls whether the recognizer section is ultimately displayed.
return ( return (
<div data-test-subj="dataVisualizerActionsPanel"> <div data-test-subj="dataVisualizerActionsPanel">
{additionalLinks
.filter(({ id }) => generatedLinks[id] !== undefined)
.map((link) => (
<>
<LinkCard
href={generatedLinks[link.id]}
icon={link.icon}
description={link.description}
title={link.title}
data-test-subj={link.dataTestSubj}
/>
<EuiSpacer size="m" />
</>
))}
{discoverLink && ( {discoverLink && (
<> <>
<EuiTitle size="s"> <EuiTitle size="s">
@ -147,8 +146,23 @@ export const ActionsPanel: FC<Props> = ({
} }
data-test-subj="dataVisualizerViewInDiscoverCard" data-test-subj="dataVisualizerViewInDiscoverCard"
/> />
<EuiSpacer size="m" />
</> </>
)} )}
{Array.isArray(asyncHrefCards) &&
asyncHrefCards.map((link) => (
<>
<LinkCard
href={link.href}
icon={link.icon}
description={link.description}
title={link.title}
data-test-subj={link['data-test-subj']}
/>
<EuiSpacer size="m" />
</>
))}
</div> </div>
); );
}; };

View file

@ -48,7 +48,7 @@ import { DatePickerWrapper } from '../../../common/components/date_picker_wrappe
import { HelpMenu } from '../../../common/components/help_menu'; import { HelpMenu } from '../../../common/components/help_menu';
import { createMergedEsQuery } from '../../utils/saved_search_utils'; import { createMergedEsQuery } from '../../utils/saved_search_utils';
import { DataVisualizerDataViewManagement } from '../data_view_management'; import { DataVisualizerDataViewManagement } from '../data_view_management';
import { ResultLink } from '../../../common/components/results_links'; import { GetAdditionalLinks } from '../../../common/components/results_links';
import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data'; import { useDataVisualizerGridData } from '../../hooks/use_data_visualizer_grid_data';
import { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable'; import { DataVisualizerGridInput } from '../../embeddables/grid_embeddable/grid_embeddable';
import './_index.scss'; import './_index.scss';
@ -110,7 +110,7 @@ export interface IndexDataVisualizerViewProps {
currentDataView: DataView; currentDataView: DataView;
currentSavedSearch: SavedSearchSavedObject | null; currentSavedSearch: SavedSearchSavedObject | null;
currentSessionId?: string; currentSessionId?: string;
additionalLinks?: ResultLink[]; getAdditionalLinks?: GetAdditionalLinks;
} }
const restorableDefaults = getDefaultDataVisualizerListState(); const restorableDefaults = getDefaultDataVisualizerListState();
@ -129,7 +129,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
dataVisualizerProps.currentSavedSearch dataVisualizerProps.currentSavedSearch
); );
const { currentDataView, additionalLinks, currentSessionId } = dataVisualizerProps; const { currentDataView, currentSessionId, getAdditionalLinks } = dataVisualizerProps;
useEffect(() => { useEffect(() => {
if (dataVisualizerProps?.currentSavedSearch !== undefined) { if (dataVisualizerProps?.currentSavedSearch !== undefined) {
@ -487,7 +487,7 @@ export const IndexDataVisualizerView: FC<IndexDataVisualizerViewProps> = (dataVi
dataView={currentDataView} dataView={currentDataView}
searchQueryLanguage={searchQueryLanguage} searchQueryLanguage={searchQueryLanguage}
searchString={searchString} searchString={searchString}
additionalLinks={additionalLinks ?? []} getAdditionalLinks={getAdditionalLinks}
/> />
</EuiFlexItem> </EuiFlexItem>
</EuiFlexGroup> </EuiFlexGroup>

View file

@ -29,17 +29,16 @@ import {
isRisonSerializationRequired, isRisonSerializationRequired,
} from '../common/util/url_state'; } from '../common/util/url_state';
import { useDataVisualizerKibana } from '../kibana_context'; import { useDataVisualizerKibana } from '../kibana_context';
import { ResultLink } from '../common/components/results_links'; import { GetAdditionalLinks } from '../common/components/results_links';
import { DATA_VISUALIZER_APP_LOCATOR, IndexDataVisualizerLocatorParams } from './locator'; import { DATA_VISUALIZER_APP_LOCATOR, IndexDataVisualizerLocatorParams } from './locator';
import { DATA_VISUALIZER_INDEX_VIEWER } from './constants/index_data_visualizer_viewer'; import { DATA_VISUALIZER_INDEX_VIEWER } from './constants/index_data_visualizer_viewer';
import { INDEX_DATA_VISUALIZER_NAME } from '../common/constants'; import { INDEX_DATA_VISUALIZER_NAME } from '../common/constants';
export type IndexDataVisualizerSpec = typeof IndexDataVisualizer;
export interface DataVisualizerUrlStateContextProviderProps { export interface DataVisualizerUrlStateContextProviderProps {
IndexDataVisualizerComponent: FC<IndexDataVisualizerViewProps>; IndexDataVisualizerComponent: FC<IndexDataVisualizerViewProps>;
additionalLinks: ResultLink[]; getAdditionalLinks?: GetAdditionalLinks;
} }
export type IndexDataVisualizerSpec = typeof IndexDataVisualizer;
export const getLocatorParams = (params: { export const getLocatorParams = (params: {
dataViewId?: string; dataViewId?: string;
@ -73,7 +72,7 @@ export const getLocatorParams = (params: {
export const DataVisualizerUrlStateContextProvider: FC< export const DataVisualizerUrlStateContextProvider: FC<
DataVisualizerUrlStateContextProviderProps DataVisualizerUrlStateContextProviderProps
> = ({ IndexDataVisualizerComponent, additionalLinks }) => { > = ({ IndexDataVisualizerComponent, getAdditionalLinks }) => {
const { services } = useDataVisualizerKibana(); const { services } = useDataVisualizerKibana();
const { const {
data: { dataViews, search }, data: { dataViews, search },
@ -247,8 +246,8 @@ export const DataVisualizerUrlStateContextProvider: FC<
<IndexDataVisualizerComponent <IndexDataVisualizerComponent
currentDataView={currentDataView} currentDataView={currentDataView}
currentSavedSearch={currentSavedSearch} currentSavedSearch={currentSavedSearch}
additionalLinks={additionalLinks}
currentSessionId={currentSessionId} currentSessionId={currentSessionId}
getAdditionalLinks={getAdditionalLinks}
/> />
) : ( ) : (
<div /> <div />
@ -257,7 +256,9 @@ export const DataVisualizerUrlStateContextProvider: FC<
); );
}; };
export const IndexDataVisualizer: FC<{ additionalLinks: ResultLink[] }> = ({ additionalLinks }) => { export const IndexDataVisualizer: FC<{
getAdditionalLinks?: GetAdditionalLinks;
}> = ({ getAdditionalLinks }) => {
const coreStart = getCoreStart(); const coreStart = getCoreStart();
const { const {
data, data,
@ -294,7 +295,7 @@ export const IndexDataVisualizer: FC<{ additionalLinks: ResultLink[] }> = ({ add
<KibanaContextProvider services={{ ...services }}> <KibanaContextProvider services={{ ...services }}>
<DataVisualizerUrlStateContextProvider <DataVisualizerUrlStateContextProvider
IndexDataVisualizerComponent={IndexDataVisualizerView} IndexDataVisualizerComponent={IndexDataVisualizerView}
additionalLinks={additionalLinks} getAdditionalLinks={getAdditionalLinks}
/> />
</KibanaContextProvider> </KibanaContextProvider>
</KibanaThemeProvider> </KibanaThemeProvider>

View file

@ -18,4 +18,8 @@ export type {
IndexDataVisualizerSpec, IndexDataVisualizerSpec,
IndexDataVisualizerViewProps, IndexDataVisualizerViewProps,
} from './application'; } from './application';
export type { ResultLink } from './application/common/components/results_links'; export type {
GetAdditionalLinksParams,
ResultLink,
GetAdditionalLinks,
} from './application/common/components/results_links';

View file

@ -38,6 +38,7 @@ export const ML_PAGES = {
*/ */
DATA_VISUALIZER_INDEX_VIEWER: 'jobs/new_job/datavisualizer', DATA_VISUALIZER_INDEX_VIEWER: 'jobs/new_job/datavisualizer',
ANOMALY_DETECTION_CREATE_JOB: `jobs/new_job`, ANOMALY_DETECTION_CREATE_JOB: `jobs/new_job`,
ANOMALY_DETECTION_CREATE_JOB_RECOGNIZER: `jobs/new_job/recognize`,
ANOMALY_DETECTION_CREATE_JOB_ADVANCED: `jobs/new_job/advanced`, ANOMALY_DETECTION_CREATE_JOB_ADVANCED: `jobs/new_job/advanced`,
ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: `jobs/new_job/step/job_type`, ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE: `jobs/new_job/step/job_type`,
ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX: `jobs/new_job/step/index_or_search`, ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX: `jobs/new_job/step/index_or_search`,

View file

@ -46,6 +46,7 @@ export interface MlGenericUrlPageState extends MlIndexBasedSearchState {
export type MlGenericUrlState = MLPageState< export type MlGenericUrlState = MLPageState<
| typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER | typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_RECOGNIZER
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX | typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX

View file

@ -39,7 +39,7 @@ export const LinkCard: FC<Props> = ({
onClick, onClick,
href, href,
isDisabled, isDisabled,
'data-test-subj': dateTestSubj, 'data-test-subj': dataTestSubj,
}) => { }) => {
const linkHrefAndOnClickProps = { const linkHrefAndOnClickProps = {
...(href ? { href } : {}), ...(href ? { href } : {}),
@ -58,7 +58,7 @@ export const LinkCard: FC<Props> = ({
background: 'transparent', background: 'transparent',
outline: 'none', outline: 'none',
}} }}
data-test-subj={dateTestSubj} data-test-subj={dataTestSubj}
color="subdued" color="subdued"
{...linkHrefAndOnClickProps} {...linkHrefAndOnClickProps}
> >

View file

@ -8,7 +8,11 @@
import React, { FC, Fragment, useState, useEffect, useMemo } from 'react'; import React, { FC, Fragment, useState, useEffect, useMemo } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import type { ResultLink, FileDataVisualizerSpec } from '@kbn/data-visualizer-plugin/public'; import type {
FileDataVisualizerSpec,
GetAdditionalLinksParams,
GetAdditionalLinks,
} from '@kbn/data-visualizer-plugin/public';
import { useTimefilter } from '../../contexts/kibana'; import { useTimefilter } from '../../contexts/kibana';
import { HelpMenu } from '../../components/help_menu'; import { HelpMenu } from '../../components/help_menu';
import { useMlKibana, useMlLocator } from '../../contexts/kibana'; import { useMlKibana, useMlLocator } from '../../contexts/kibana';
@ -19,11 +23,6 @@ import { mlNodesAvailable, getMlNodeCount } from '../../ml_nodes_check/check_ml_
import { checkPermission } from '../../capabilities/check_capabilities'; import { checkPermission } from '../../capabilities/check_capabilities';
import { MlPageHeader } from '../../components/page_header'; import { MlPageHeader } from '../../components/page_header';
interface GetUrlParams {
indexPatternId: string;
globalState: any;
}
export const FileDataVisualizerPage: FC = () => { export const FileDataVisualizerPage: FC = () => {
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
const { const {
@ -40,60 +39,62 @@ export const FileDataVisualizerPage: FC = () => {
const [FileDataVisualizer, setFileDataVisualizer] = useState<FileDataVisualizerSpec | null>(null); const [FileDataVisualizer, setFileDataVisualizer] = useState<FileDataVisualizerSpec | null>(null);
const links: ResultLink[] = useMemo( const getAdditionalLinks: GetAdditionalLinks = useMemo(
() => [ () => [
{ async ({ dataViewId, globalState }: GetAdditionalLinksParams) => [
id: 'create_ml_job', {
title: i18n.translate('xpack.ml.fileDatavisualizer.actionsPanel.anomalyDetectionTitle', { id: 'create_ml_job',
defaultMessage: 'Create new ML job', title: i18n.translate('xpack.ml.fileDatavisualizer.actionsPanel.anomalyDetectionTitle', {
}), defaultMessage: 'Create ML job',
description: '', }),
icon: 'machineLearningApp', description: '',
type: 'file', icon: 'machineLearningApp',
getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { type: 'file',
return await mlLocator.getUrl({ getUrl: async () => {
page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE, return await mlLocator.getUrl({
pageState: { page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE,
index: indexPatternId, pageState: {
globalState, index: dataViewId,
}, globalState,
}); },
});
},
canDisplay: async () => {
try {
const { timeFieldName } = await getDataView(dataViewId);
return (
isFullLicense() &&
timeFieldName !== undefined &&
checkPermission('canCreateJob') &&
mlNodesAvailable()
);
} catch (error) {
return false;
}
},
}, },
canDisplay: async ({ indexPatternId }) => { {
try { id: 'open_in_data_viz',
const { timeFieldName } = await getDataView(indexPatternId); title: i18n.translate('xpack.ml.fileDatavisualizer.actionsPanel.dataframeTitle', {
return ( defaultMessage: 'Open in Data Visualizer',
isFullLicense() && }),
timeFieldName !== undefined && description: '',
checkPermission('canCreateJob') && icon: 'dataVisualizer',
mlNodesAvailable() type: 'file',
); getUrl: async () => {
} catch (error) { return await mlLocator.getUrl({
return false; page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER,
} pageState: {
index: dataViewId,
globalState,
},
});
},
canDisplay: async () => dataViewId !== '',
}, },
}, ],
{
id: 'open_in_data_viz',
title: i18n.translate('xpack.ml.fileDatavisualizer.actionsPanel.dataframeTitle', {
defaultMessage: 'Open in Data Visualizer',
}),
description: '',
icon: 'dataVisualizer',
type: 'file',
getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => {
return await mlLocator.getUrl({
page: ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER,
pageState: {
index: indexPatternId,
globalState,
},
});
},
canDisplay: async ({ indexPatternId }) => indexPatternId !== '',
},
], ],
[] [mlLocator]
); );
useEffect(() => { useEffect(() => {
@ -114,7 +115,7 @@ export const FileDataVisualizerPage: FC = () => {
defaultMessage="Data Visualizer" defaultMessage="Data Visualizer"
/> />
</MlPageHeader> </MlPageHeader>
<FileDataVisualizer additionalLinks={links} /> <FileDataVisualizer getAdditionalLinks={getAdditionalLinks} />
</> </>
) : null} ) : null}
<HelpMenu docLink={docLinks.links.ml.guide} /> <HelpMenu docLink={docLinks.links.ml.guide} />

View file

@ -5,28 +5,38 @@
* 2.0. * 2.0.
*/ */
import React, { FC, Fragment, useEffect, useState, useMemo } from 'react'; import React, { FC, Fragment, useEffect, useMemo, useState } from 'react';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react'; import { FormattedMessage } from '@kbn/i18n-react';
import type { ResultLink, IndexDataVisualizerSpec } from '@kbn/data-visualizer-plugin/public'; import type {
IndexDataVisualizerSpec,
ResultLink,
GetAdditionalLinks,
GetAdditionalLinksParams,
} from '@kbn/data-visualizer-plugin/public';
import { useMlKibana, useTimefilter, useMlLocator } from '../../contexts/kibana'; import { useMlKibana, useTimefilter, useMlLocator } from '../../contexts/kibana';
import { HelpMenu } from '../../components/help_menu'; import { HelpMenu } from '../../components/help_menu';
import { ML_PAGES } from '../../../../common/constants/locator'; import { ML_PAGES } from '../../../../common/constants/locator';
import { isFullLicense } from '../../license'; import { isFullLicense } from '../../license';
import { mlNodesAvailable, getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes'; import { mlNodesAvailable, getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes';
import { checkPermission } from '../../capabilities/check_capabilities'; import { checkPermission } from '../../capabilities/check_capabilities';
import { MlPageHeader } from '../../components/page_header'; import { MlPageHeader } from '../../components/page_header';
interface GetUrlParams { interface RecognizerModule {
indexPatternId: string; id: string;
globalState: any; title: string;
query: Record<string, object>;
description: string;
logo: {
icon: string;
};
} }
export const IndexDataVisualizerPage: FC = () => { export const IndexDataVisualizerPage: FC = () => {
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false }); useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
const { const {
services: { services: {
http,
docLinks, docLinks,
dataVisualizer, dataVisualizer,
data: { data: {
@ -48,8 +58,12 @@ export const IndexDataVisualizerPage: FC = () => {
} }
}, []); }, []);
const links: ResultLink[] = useMemo( const getAsyncMLCards = async ({
() => [ dataViewId,
dataViewTitle,
globalState,
}: GetAdditionalLinksParams): Promise<ResultLink[]> => {
return [
{ {
id: 'create_ml_ad_job', id: 'create_ml_ad_job',
title: i18n.translate('xpack.ml.indexDatavisualizer.actionsPanel.anomalyDetectionTitle', { title: i18n.translate('xpack.ml.indexDatavisualizer.actionsPanel.anomalyDetectionTitle', {
@ -64,18 +78,18 @@ export const IndexDataVisualizerPage: FC = () => {
), ),
icon: 'createAdvancedJob', icon: 'createAdvancedJob',
type: 'file', type: 'file',
getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { getUrl: async () => {
return await mlLocator.getUrl({ return await mlLocator.getUrl({
page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED, page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED,
pageState: { pageState: {
index: indexPatternId, index: dataViewId,
globalState, globalState,
}, },
}); });
}, },
canDisplay: async ({ indexPatternId }) => { canDisplay: async () => {
try { try {
const { timeFieldName } = await getDataView(indexPatternId); const { timeFieldName } = await getDataView(dataViewId);
return ( return (
isFullLicense() && isFullLicense() &&
timeFieldName !== undefined && timeFieldName !== undefined &&
@ -86,7 +100,7 @@ export const IndexDataVisualizerPage: FC = () => {
return false; return false;
} }
}, },
dataTestSubj: 'dataVisualizerCreateAdvancedJobCard', 'data-test-subj': 'dataVisualizerCreateAdvancedJobCard',
}, },
{ {
id: 'create_ml_dfa_job', id: 'create_ml_dfa_job',
@ -101,11 +115,11 @@ export const IndexDataVisualizerPage: FC = () => {
), ),
icon: 'classificationJob', icon: 'classificationJob',
type: 'file', type: 'file',
getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => { getUrl: async () => {
return await mlLocator.getUrl({ return await mlLocator.getUrl({
page: ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB, page: ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB,
pageState: { pageState: {
index: indexPatternId, index: dataViewId,
globalState, globalState,
}, },
}); });
@ -115,12 +129,57 @@ export const IndexDataVisualizerPage: FC = () => {
isFullLicense() && checkPermission('canCreateDataFrameAnalytics') && mlNodesAvailable() isFullLicense() && checkPermission('canCreateDataFrameAnalytics') && mlNodesAvailable()
); );
}, },
dataTestSubj: 'dataVisualizerCreateDataFrameAnalyticsCard', 'data-test-subj': 'dataVisualizerCreateDataFrameAnalyticsCard',
}, },
], ];
[] };
);
const getAsyncRecognizedModuleCards = async (params: GetAdditionalLinksParams) => {
const { dataViewId, dataViewTitle } = params;
const modules = await http.fetch<RecognizerModule[]>(
`/api/ml/modules/recognize/${dataViewTitle}`,
{
method: 'GET',
}
);
return modules?.map(
(m): ResultLink => ({
id: m.id,
title: m.title,
description: m.description,
icon: m.logo.icon,
type: 'index',
getUrl: async () => {
return await mlLocator.getUrl({
page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_RECOGNIZER,
pageState: {
id: m.id,
index: dataViewId,
},
});
},
canDisplay: async () => {
try {
const { timeFieldName } = await getDataView(dataViewId);
return (
isFullLicense() &&
timeFieldName !== undefined &&
checkPermission('canCreateJob') &&
mlNodesAvailable()
);
} catch (error) {
return false;
}
},
'data-test-subj': m.id,
})
);
};
const getAdditionalLinks: GetAdditionalLinks = useMemo(
() => [getAsyncRecognizedModuleCards, getAsyncMLCards],
[mlLocator]
);
return IndexDataVisualizer ? ( return IndexDataVisualizer ? (
<Fragment> <Fragment>
{IndexDataVisualizer !== null ? ( {IndexDataVisualizer !== null ? (
@ -131,7 +190,7 @@ export const IndexDataVisualizerPage: FC = () => {
defaultMessage="Data Visualizer" defaultMessage="Data Visualizer"
/> />
</MlPageHeader> </MlPageHeader>
<IndexDataVisualizer additionalLinks={links} /> <IndexDataVisualizer getAdditionalLinks={getAdditionalLinks} />
</> </>
) : null} ) : null}
<HelpMenu docLink={docLinks.links.ml.guide} /> <HelpMenu docLink={docLinks.links.ml.guide} />

View file

@ -77,6 +77,7 @@ export class MlLocatorDefinition implements LocatorDefinition<MlLocatorParams> {
path = formatTrainedModelsNodesManagementUrl('', params.pageState); path = formatTrainedModelsNodesManagementUrl('', params.pageState);
break; break;
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB: case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB:
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_RECOGNIZER:
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED: case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED:
case ML_PAGES.DATA_VISUALIZER: case ML_PAGES.DATA_VISUALIZER:
case ML_PAGES.DATA_VISUALIZER_FILE: case ML_PAGES.DATA_VISUALIZER_FILE: