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

View file

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

View file

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

View file

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

View file

@ -11,28 +11,31 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { EuiSpacer, EuiTitle } from '@elastic/eui';
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 { useUrlState } from '../../../common/util/url_state';
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 {
dataView: DataView;
searchString?: string | { [key: string]: any };
searchQueryLanguage?: string;
additionalLinks: ResultLink[];
getAdditionalLinks?: GetAdditionalLinks;
}
export const ActionsPanel: FC<Props> = ({
dataView,
searchString,
searchQueryLanguage,
additionalLinks,
getAdditionalLinks,
}) => {
const [globalState] = useUrlState('_g');
const [discoverLink, setDiscoverLink] = useState('');
const [generatedLinks, setGeneratedLinks] = useState<Record<string, string>>({});
const [asyncHrefCards, setAsyncHrefCards] = useState<LinkCardProps[]>();
const {
services: {
@ -46,6 +49,7 @@ export const ActionsPanel: FC<Props> = ({
let unmounted = false;
const indexPatternId = dataView.id;
const indexPatternTitle = dataView.title;
const getDiscoverUrl = async (): Promise<void> => {
const isDiscoverAvailable = capabilities.discover?.show ?? false;
if (!isDiscoverAvailable) return;
@ -68,24 +72,33 @@ export const ActionsPanel: FC<Props> = ({
setDiscoverLink(discoverUrl);
};
Promise.all(
additionalLinks.map(async ({ canDisplay, getUrl }) => {
if ((await canDisplay({ indexPatternId })) === false) {
return null;
}
return getUrl({ globalState, indexPatternId });
})
).then((urls) => {
const linksById = urls.reduce((acc, url, i) => {
if (url !== null) {
acc[additionalLinks[i].id] = url;
}
return acc;
}, {} as Record<string, string>);
setGeneratedLinks(linksById);
});
if (Array.isArray(getAdditionalLinks) && indexPatternId !== undefined) {
Promise.all(
getAdditionalLinks.map(async (asyncCardGetter) => {
const results = await asyncCardGetter({
dataViewId: indexPatternId,
dataViewTitle: indexPatternTitle,
});
if (Array.isArray(results)) {
return await Promise.all(
results.map(async (c) => ({
...c,
canDisplay: await c.canDisplay(),
href: await c.getUrl(),
}))
);
}
})
).then((cards) => {
setAsyncHrefCards(
flatten(cards)
.filter(isDefined)
.filter((d) => d.canDisplay === true)
);
});
}
getDiscoverUrl();
return () => {
unmounted = true;
};
@ -96,8 +109,8 @@ export const ActionsPanel: FC<Props> = ({
globalState,
capabilities,
discover,
additionalLinks,
data.query,
getAdditionalLinks,
]);
// 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.
return (
<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 && (
<>
<EuiTitle size="s">
@ -147,8 +146,23 @@ export const ActionsPanel: FC<Props> = ({
}
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>
);
};

View file

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

View file

@ -29,17 +29,16 @@ import {
isRisonSerializationRequired,
} from '../common/util/url_state';
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_INDEX_VIEWER } from './constants/index_data_visualizer_viewer';
import { INDEX_DATA_VISUALIZER_NAME } from '../common/constants';
export type IndexDataVisualizerSpec = typeof IndexDataVisualizer;
export interface DataVisualizerUrlStateContextProviderProps {
IndexDataVisualizerComponent: FC<IndexDataVisualizerViewProps>;
additionalLinks: ResultLink[];
getAdditionalLinks?: GetAdditionalLinks;
}
export type IndexDataVisualizerSpec = typeof IndexDataVisualizer;
export const getLocatorParams = (params: {
dataViewId?: string;
@ -73,7 +72,7 @@ export const getLocatorParams = (params: {
export const DataVisualizerUrlStateContextProvider: FC<
DataVisualizerUrlStateContextProviderProps
> = ({ IndexDataVisualizerComponent, additionalLinks }) => {
> = ({ IndexDataVisualizerComponent, getAdditionalLinks }) => {
const { services } = useDataVisualizerKibana();
const {
data: { dataViews, search },
@ -247,8 +246,8 @@ export const DataVisualizerUrlStateContextProvider: FC<
<IndexDataVisualizerComponent
currentDataView={currentDataView}
currentSavedSearch={currentSavedSearch}
additionalLinks={additionalLinks}
currentSessionId={currentSessionId}
getAdditionalLinks={getAdditionalLinks}
/>
) : (
<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 {
data,
@ -294,7 +295,7 @@ export const IndexDataVisualizer: FC<{ additionalLinks: ResultLink[] }> = ({ add
<KibanaContextProvider services={{ ...services }}>
<DataVisualizerUrlStateContextProvider
IndexDataVisualizerComponent={IndexDataVisualizerView}
additionalLinks={additionalLinks}
getAdditionalLinks={getAdditionalLinks}
/>
</KibanaContextProvider>
</KibanaThemeProvider>

View file

@ -18,4 +18,8 @@ export type {
IndexDataVisualizerSpec,
IndexDataVisualizerViewProps,
} 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',
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_SELECT_TYPE: `jobs/new_job/step/job_type`,
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<
| typeof ML_PAGES.DATA_VISUALIZER_INDEX_VIEWER
| 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_SELECT_TYPE
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX

View file

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

View file

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

View file

@ -5,28 +5,38 @@
* 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 { 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 { HelpMenu } from '../../components/help_menu';
import { ML_PAGES } from '../../../../common/constants/locator';
import { isFullLicense } from '../../license';
import { mlNodesAvailable, getMlNodeCount } from '../../ml_nodes_check/check_ml_nodes';
import { checkPermission } from '../../capabilities/check_capabilities';
import { MlPageHeader } from '../../components/page_header';
interface GetUrlParams {
indexPatternId: string;
globalState: any;
interface RecognizerModule {
id: string;
title: string;
query: Record<string, object>;
description: string;
logo: {
icon: string;
};
}
export const IndexDataVisualizerPage: FC = () => {
useTimefilter({ timeRangeSelector: false, autoRefreshSelector: false });
const {
services: {
http,
docLinks,
dataVisualizer,
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',
title: i18n.translate('xpack.ml.indexDatavisualizer.actionsPanel.anomalyDetectionTitle', {
@ -64,18 +78,18 @@ export const IndexDataVisualizerPage: FC = () => {
),
icon: 'createAdvancedJob',
type: 'file',
getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => {
getUrl: async () => {
return await mlLocator.getUrl({
page: ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED,
pageState: {
index: indexPatternId,
index: dataViewId,
globalState,
},
});
},
canDisplay: async ({ indexPatternId }) => {
canDisplay: async () => {
try {
const { timeFieldName } = await getDataView(indexPatternId);
const { timeFieldName } = await getDataView(dataViewId);
return (
isFullLicense() &&
timeFieldName !== undefined &&
@ -86,7 +100,7 @@ export const IndexDataVisualizerPage: FC = () => {
return false;
}
},
dataTestSubj: 'dataVisualizerCreateAdvancedJobCard',
'data-test-subj': 'dataVisualizerCreateAdvancedJobCard',
},
{
id: 'create_ml_dfa_job',
@ -101,11 +115,11 @@ export const IndexDataVisualizerPage: FC = () => {
),
icon: 'classificationJob',
type: 'file',
getUrl: async ({ indexPatternId, globalState }: GetUrlParams) => {
getUrl: async () => {
return await mlLocator.getUrl({
page: ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB,
pageState: {
index: indexPatternId,
index: dataViewId,
globalState,
},
});
@ -115,12 +129,57 @@ export const IndexDataVisualizerPage: FC = () => {
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 ? (
<Fragment>
{IndexDataVisualizer !== null ? (
@ -131,7 +190,7 @@ export const IndexDataVisualizerPage: FC = () => {
defaultMessage="Data Visualizer"
/>
</MlPageHeader>
<IndexDataVisualizer additionalLinks={links} />
<IndexDataVisualizer getAdditionalLinks={getAdditionalLinks} />
</>
) : null}
<HelpMenu docLink={docLinks.links.ml.guide} />

View file

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