mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
* [ML] allocated models as a table * [ML] handle fetching stats for stopped model * [ML] change date formatter, enable check for start and stop action, change icon * [ML] hide stats tab when not stats are available * [ML] update state on action * [ML] update memory breakdown message * [ML] fix type * [ML] fix functional test * [ML] render nodes as links * [ML] fix locator types * [ML] support node name in the URL state * [ML] update icon * [ML] routing state with reason tooltip * [ML] move experimental tag * [ML] add state column, fix import * [ML] update stats on aciton * [ML] refresh by timer * [ML] rename fetchStats callback * [ML] replace with promise.all * [ML] add deployment state const Co-authored-by: Dima Arnautov <dmitrii.arnautov@elastic.co>
This commit is contained in:
parent
e5960af1ad
commit
a11ae4949e
15 changed files with 610 additions and 272 deletions
14
x-pack/plugins/ml/common/constants/trained_models.ts
Normal file
14
x-pack/plugins/ml/common/constants/trained_models.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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 const DEPLOYMENT_STATE = {
|
||||
STARTED: 'started',
|
||||
STARTING: 'starting',
|
||||
STOPPING: 'stopping',
|
||||
} as const;
|
||||
|
||||
export type DeploymentState = typeof DEPLOYMENT_STATE[keyof typeof DEPLOYMENT_STATE];
|
|
@ -188,6 +188,10 @@ export interface TrainedModelsQueryState {
|
|||
modelId?: string;
|
||||
}
|
||||
|
||||
export interface TrainedModelsNodesQueryState {
|
||||
nodeId?: string;
|
||||
}
|
||||
|
||||
export type DataFrameAnalyticsUrlState = MLPageState<
|
||||
| typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE
|
||||
| typeof ML_PAGES.DATA_FRAME_ANALYTICS_MAP
|
||||
|
@ -255,7 +259,8 @@ export type MlLocatorState =
|
|||
| CalendarEditUrlState
|
||||
| FilterEditUrlState
|
||||
| MlGenericUrlState
|
||||
| TrainedModelsUrlState;
|
||||
| TrainedModelsUrlState
|
||||
| TrainedModelsNodesUrlState;
|
||||
|
||||
export type MlLocatorParams = MlLocatorState & SerializableRecord;
|
||||
|
||||
|
@ -265,3 +270,8 @@ export type TrainedModelsUrlState = MLPageState<
|
|||
typeof ML_PAGES.TRAINED_MODELS_MANAGE,
|
||||
TrainedModelsQueryState | undefined
|
||||
>;
|
||||
|
||||
export type TrainedModelsNodesUrlState = MLPageState<
|
||||
typeof ML_PAGES.TRAINED_MODELS_NODES,
|
||||
TrainedModelsNodesQueryState | undefined
|
||||
>;
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataFrameAnalyticsConfig } from './data_frame_analytics';
|
||||
import { FeatureImportanceBaseline, TotalFeatureImportance } from './feature_importance';
|
||||
import { XOR } from './common';
|
||||
import type { DataFrameAnalyticsConfig } from './data_frame_analytics';
|
||||
import type { FeatureImportanceBaseline, TotalFeatureImportance } from './feature_importance';
|
||||
import type { XOR } from './common';
|
||||
import type { DeploymentState } from '../constants/trained_models';
|
||||
|
||||
export interface IngestStats {
|
||||
count: number;
|
||||
|
@ -17,8 +18,8 @@ export interface IngestStats {
|
|||
}
|
||||
|
||||
export interface TrainedModelStat {
|
||||
model_id: string;
|
||||
pipeline_count: number;
|
||||
model_id?: string;
|
||||
pipeline_count?: number;
|
||||
inference_stats?: {
|
||||
failure_count: number;
|
||||
inference_count: number;
|
||||
|
@ -100,6 +101,9 @@ export interface TrainedModelConfigResponse {
|
|||
tags: string[];
|
||||
version: string;
|
||||
inference_config?: Record<string, any>;
|
||||
/**
|
||||
* Associated pipelines. Extends response from the ES endpoint.
|
||||
*/
|
||||
pipelines?: Record<string, PipelineDefinition> | null;
|
||||
}
|
||||
|
||||
|
@ -125,7 +129,7 @@ export interface TrainedModelDeploymentStatsResponse {
|
|||
model_size_bytes: number;
|
||||
inference_threads: number;
|
||||
model_threads: number;
|
||||
state: string;
|
||||
state: DeploymentState;
|
||||
allocation_status: { target_allocation_count: number; state: string; allocation_count: number };
|
||||
nodes: Array<{
|
||||
node: Record<
|
||||
|
@ -150,24 +154,35 @@ export interface TrainedModelDeploymentStatsResponse {
|
|||
}>;
|
||||
}
|
||||
|
||||
export interface AllocatedModel {
|
||||
inference_threads: number;
|
||||
allocation_status: {
|
||||
target_allocation_count: number;
|
||||
state: string;
|
||||
allocation_count: number;
|
||||
};
|
||||
model_id: string;
|
||||
state: string;
|
||||
model_threads: number;
|
||||
model_size_bytes: number;
|
||||
node: {
|
||||
average_inference_time_ms: number;
|
||||
inference_count: number;
|
||||
routing_state: {
|
||||
routing_state: string;
|
||||
reason?: string;
|
||||
};
|
||||
last_access?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NodeDeploymentStatsResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
transport_address: string;
|
||||
attributes: Record<string, string>;
|
||||
roles: string[];
|
||||
allocated_models: Array<{
|
||||
inference_threads: number;
|
||||
allocation_status: {
|
||||
target_allocation_count: number;
|
||||
state: string;
|
||||
allocation_count: number;
|
||||
};
|
||||
model_id: string;
|
||||
state: string;
|
||||
model_threads: number;
|
||||
model_size_bytes: number;
|
||||
}>;
|
||||
allocated_models: AllocatedModel[];
|
||||
memory_overview: {
|
||||
machine_memory: {
|
||||
/** Total machine memory in bytes */
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import React, { FC, useState, useEffect } from 'react';
|
||||
|
||||
import { EuiPageHeader, EuiBetaBadge } from '@elastic/eui';
|
||||
import { EuiPageHeader } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { TabId } from './navigation_menu';
|
||||
import { useMlKibana, useMlLocator, useNavigateToPath } from '../../contexts/kibana';
|
||||
|
@ -57,20 +57,6 @@ function getTabs(disableLinks: boolean): Tab[] {
|
|||
defaultMessage: 'Model Management',
|
||||
}),
|
||||
disabled: disableLinks,
|
||||
betaTag: (
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate('xpack.ml.navMenu.trainedModelsTabBetaLabel', {
|
||||
defaultMessage: 'Experimental',
|
||||
})}
|
||||
size="m"
|
||||
color="hollow"
|
||||
iconType="beaker"
|
||||
tooltipContent={i18n.translate('xpack.ml.navMenu.trainedModelsTabBetaTooltipContent', {
|
||||
defaultMessage:
|
||||
"Model Management is an experimental feature and subject to change. We'd love to hear your feedback.",
|
||||
})}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'datavisualizer',
|
||||
|
@ -201,7 +187,6 @@ export const MainTabs: FC<Props> = ({ tabId, disableLinks }) => {
|
|||
},
|
||||
'data-test-subj': testSubject + (id === selectedTabId ? ' selected' : ''),
|
||||
isSelected: id === selectedTabId,
|
||||
append: tab.betaTag,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
|
|
|
@ -6,12 +6,26 @@
|
|||
*/
|
||||
|
||||
import { useMlKibana } from './kibana_context';
|
||||
import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common';
|
||||
|
||||
export function useFieldFormatter(fieldType: 'bytes') {
|
||||
/**
|
||||
* Set of reasonable defaults for formatters for the ML app.
|
||||
*/
|
||||
const defaultParam = {
|
||||
[FIELD_FORMAT_IDS.DURATION]: {
|
||||
inputFormat: 'milliseconds',
|
||||
outputFormat: 'humanizePrecise',
|
||||
},
|
||||
} as Record<FIELD_FORMAT_IDS, object | undefined>;
|
||||
|
||||
export function useFieldFormatter(fieldType: FIELD_FORMAT_IDS) {
|
||||
const {
|
||||
services: { fieldFormats },
|
||||
} = useMlKibana();
|
||||
|
||||
const fieldFormatter = fieldFormats.deserialize({ id: fieldType });
|
||||
const fieldFormatter = fieldFormats.deserialize({
|
||||
id: fieldType,
|
||||
params: defaultParam[fieldType],
|
||||
});
|
||||
return fieldFormatter.convert.bind(fieldFormatter);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, Fragment } from 'react';
|
||||
import React, { FC, Fragment, useEffect, useState } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiButtonEmpty,
|
||||
|
@ -15,6 +16,7 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiListGroup,
|
||||
EuiNotificationBadge,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
|
@ -25,11 +27,13 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import type { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item';
|
||||
import { ModelItemFull } from './models_list';
|
||||
import { useMlKibana } from '../../contexts/kibana';
|
||||
import { useMlKibana, useMlLocator } from '../../contexts/kibana';
|
||||
import { timeFormatter } from '../../../../common/util/date_utils';
|
||||
import { isDefined } from '../../../../common/types/guards';
|
||||
import { isPopulatedObject } from '../../../../common';
|
||||
import { ML_PAGES } from '../../../../common/constants/locator';
|
||||
|
||||
interface ExpandedRowProps {
|
||||
item: ModelItemFull;
|
||||
|
@ -85,6 +89,12 @@ export function formatToListItems(
|
|||
}
|
||||
|
||||
export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
|
||||
const mlLocator = useMlLocator();
|
||||
|
||||
const [deploymentStatsItems, setDeploymentStats] = useState<EuiDescriptionListProps['listItems']>(
|
||||
[]
|
||||
);
|
||||
|
||||
const {
|
||||
inference_config: inferenceConfig,
|
||||
stats,
|
||||
|
@ -119,6 +129,42 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
|
|||
services: { share },
|
||||
} = useMlKibana();
|
||||
|
||||
useEffect(
|
||||
function updateDeploymentState() {
|
||||
(async function () {
|
||||
const { nodes, ...deploymentStats } = stats.deployment_stats ?? {};
|
||||
|
||||
if (!isPopulatedObject(deploymentStats)) return;
|
||||
|
||||
const result = formatToListItems(deploymentStats)!;
|
||||
|
||||
const items: EuiListGroupItemProps[] = await Promise.all(
|
||||
nodes!.map(async (v) => {
|
||||
const nodeObject = Object.values(v.node)[0];
|
||||
const href = await mlLocator!.getUrl({
|
||||
page: ML_PAGES.TRAINED_MODELS_NODES,
|
||||
pageState: {
|
||||
nodeId: nodeObject.name,
|
||||
},
|
||||
});
|
||||
return {
|
||||
label: nodeObject.name,
|
||||
href,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
result.push({
|
||||
title: 'nodes',
|
||||
description: <EuiListGroup size={'s'} gutterSize={'s'} listItems={items} />,
|
||||
});
|
||||
|
||||
setDeploymentStats(result);
|
||||
})();
|
||||
},
|
||||
[stats.deployment_stats]
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'details',
|
||||
|
@ -234,164 +280,168 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
|
|||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'stats',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.statsTabLabel"
|
||||
defaultMessage="Stats"
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer size={'m'} />
|
||||
{stats.deployment_stats && (
|
||||
<>
|
||||
<EuiPanel>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.deploymentStatsTitle"
|
||||
defaultMessage="Deployment stats"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
...(isPopulatedObject(omit(stats, 'pipeline_count'))
|
||||
? [
|
||||
{
|
||||
id: 'stats',
|
||||
name: (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.statsTabLabel"
|
||||
defaultMessage="Stats"
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={formatToListItems(stats.deployment_stats)}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer size={'m'} />
|
||||
</>
|
||||
)}
|
||||
<EuiFlexGrid columns={2}>
|
||||
{stats.inference_stats && (
|
||||
<EuiFlexItem>
|
||||
<EuiPanel>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.inferenceStatsTitle"
|
||||
defaultMessage="Inference stats"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={formatToListItems(stats.inference_stats)}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{stats.ingest?.total && (
|
||||
<EuiFlexItem>
|
||||
<EuiPanel style={{ maxHeight: '400px', overflow: 'auto' }}>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.ingestStatsTitle"
|
||||
defaultMessage="Ingest stats"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={formatToListItems(stats.ingest.total)}
|
||||
/>
|
||||
|
||||
{stats.ingest?.pipelines && (
|
||||
<>
|
||||
<EuiSpacer size={'m'} />
|
||||
{!!deploymentStatsItems?.length ? (
|
||||
<>
|
||||
<EuiPanel>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.byPipelineTitle"
|
||||
defaultMessage="By pipeline"
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.deploymentStatsTitle"
|
||||
defaultMessage="Deployment stats"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size={'s'} />
|
||||
{Object.entries(stats.ingest.pipelines).map(
|
||||
([pipelineName, { processors, ...pipelineStats }], i) => {
|
||||
return (
|
||||
<Fragment key={pipelineName}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size={'xs'}>
|
||||
<EuiTextColor color="subdued">
|
||||
<h5>
|
||||
{i + 1}. {pipelineName}
|
||||
</h5>
|
||||
</EuiTextColor>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule size={'full'} margin={'s'} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={formatToListItems(pipelineStats)}
|
||||
/>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiTitle size={'xxs'}>
|
||||
<h6>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.byProcessorTitle"
|
||||
defaultMessage="By processor"
|
||||
/>
|
||||
</h6>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size={'s'} />
|
||||
<>
|
||||
{processors.map((processor) => {
|
||||
const name = Object.keys(processor)[0];
|
||||
const { stats: processorStats } = processor[name];
|
||||
return (
|
||||
<Fragment key={name}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size={'xxs'}>
|
||||
<EuiTextColor color="subdued">
|
||||
<h6>{name}</h6>
|
||||
</EuiTextColor>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule size={'full'} margin={'s'} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={formatToListItems(processorStats)}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={deploymentStatsItems}
|
||||
/>
|
||||
</EuiPanel>
|
||||
<EuiSpacer size={'m'} />
|
||||
</>
|
||||
) : null}
|
||||
<EuiFlexGrid columns={2}>
|
||||
{stats.inference_stats && (
|
||||
<EuiFlexItem>
|
||||
<EuiPanel>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.inferenceStatsTitle"
|
||||
defaultMessage="Inference stats"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={formatToListItems(stats.inference_stats)}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGrid>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{stats.ingest?.total && (
|
||||
<EuiFlexItem>
|
||||
<EuiPanel style={{ maxHeight: '400px', overflow: 'auto' }}>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.ingestStatsTitle"
|
||||
defaultMessage="Ingest stats"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={formatToListItems(stats.ingest.total)}
|
||||
/>
|
||||
|
||||
{stats.ingest?.pipelines && (
|
||||
<>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiTitle size={'xs'}>
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.byPipelineTitle"
|
||||
defaultMessage="By pipeline"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size={'s'} />
|
||||
{Object.entries(stats.ingest.pipelines).map(
|
||||
([pipelineName, { processors, ...pipelineStats }], i) => {
|
||||
return (
|
||||
<Fragment key={pipelineName}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size={'xs'}>
|
||||
<EuiTextColor color="subdued">
|
||||
<h5>
|
||||
{i + 1}. {pipelineName}
|
||||
</h5>
|
||||
</EuiTextColor>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule size={'full'} margin={'s'} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={formatToListItems(pipelineStats)}
|
||||
/>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiTitle size={'xxs'}>
|
||||
<h6>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.modelsList.expandedRow.byProcessorTitle"
|
||||
defaultMessage="By processor"
|
||||
/>
|
||||
</h6>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size={'s'} />
|
||||
<>
|
||||
{processors.map((processor) => {
|
||||
const name = Object.keys(processor)[0];
|
||||
const { stats: processorStats } = processor[name];
|
||||
return (
|
||||
<Fragment key={name}>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size={'xxs'}>
|
||||
<EuiTextColor color="subdued">
|
||||
<h6>{name}</h6>
|
||||
</EuiTextColor>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule size={'full'} margin={'s'} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size={'m'} />
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={formatToListItems(processorStats)}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGrid>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(pipelines && Object.keys(pipelines).length > 0
|
||||
? [
|
||||
{
|
||||
|
|
|
@ -5,19 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { FC, useState, useCallback, useMemo } from 'react';
|
||||
import { groupBy } from 'lodash';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
EuiInMemoryTable,
|
||||
EuiBadge,
|
||||
EuiButton,
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiTitle,
|
||||
EuiButton,
|
||||
EuiSpacer,
|
||||
EuiButtonIcon,
|
||||
EuiBadge,
|
||||
SearchFilterConfig,
|
||||
EuiInMemoryTable,
|
||||
EuiSearchBarProps,
|
||||
EuiSpacer,
|
||||
EuiTitle,
|
||||
SearchFilterConfig,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
@ -48,9 +48,12 @@ import { ListingPageUrlState } from '../../../../common/types/common';
|
|||
import { usePageUrlState } from '../../util/url_state';
|
||||
import { ExpandedRow } from './expanded_row';
|
||||
import { isPopulatedObject } from '../../../../common';
|
||||
import { timeFormatter } from '../../../../common/util/date_utils';
|
||||
import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings';
|
||||
import { useToastNotificationService } from '../../services/toast_notification_service';
|
||||
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
|
||||
import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common';
|
||||
import { useRefresh } from '../../routing/use_refresh';
|
||||
import { DEPLOYMENT_STATE } from '../../../../common/constants/trained_models';
|
||||
|
||||
type Stats = Omit<TrainedModelStat, 'model_id'>;
|
||||
|
||||
|
@ -82,11 +85,15 @@ export const ModelsList: FC = () => {
|
|||
} = useMlKibana();
|
||||
const urlLocator = useMlLocator()!;
|
||||
|
||||
const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE);
|
||||
|
||||
const [pageState, updatePageState] = usePageUrlState(
|
||||
ML_PAGES.TRAINED_MODELS_MANAGE,
|
||||
getDefaultModelsListState()
|
||||
);
|
||||
|
||||
const refresh = useRefresh();
|
||||
|
||||
const searchQueryText = pageState.queryText ?? '';
|
||||
|
||||
const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean;
|
||||
|
@ -121,7 +128,7 @@ export const ModelsList: FC = () => {
|
|||
size: 1000,
|
||||
});
|
||||
|
||||
const newItems = [];
|
||||
const newItems: ModelItem[] = [];
|
||||
const expandedItemsToRefresh = [];
|
||||
|
||||
for (const model of response) {
|
||||
|
@ -145,6 +152,11 @@ export const ModelsList: FC = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Need to fetch state for 3rd party models to enable/disable actions
|
||||
await fetchAndPopulateDeploymentStats(
|
||||
newItems.filter((v) => v.model_type.includes('pytorch'))
|
||||
);
|
||||
|
||||
setItems(newItems);
|
||||
|
||||
if (expandedItemsToRefresh.length > 0) {
|
||||
|
@ -175,6 +187,13 @@ export const ModelsList: FC = () => {
|
|||
onRefresh: fetchModelsData,
|
||||
});
|
||||
|
||||
useEffect(
|
||||
function updateOnTimerRefresh() {
|
||||
fetchModelsData();
|
||||
},
|
||||
[refresh]
|
||||
);
|
||||
|
||||
const modelsStats: ModelsBarStats = useMemo(() => {
|
||||
return {
|
||||
total: {
|
||||
|
@ -191,8 +210,6 @@ export const ModelsList: FC = () => {
|
|||
* Fetches models stats and update the original object
|
||||
*/
|
||||
const fetchModelsStats = useCallback(async (models: ModelItem[]) => {
|
||||
const { true: pytorchModels } = groupBy(models, (m) => m.model_type === 'pytorch');
|
||||
|
||||
try {
|
||||
if (models) {
|
||||
const { trained_model_stats: modelsStatsResponse } =
|
||||
|
@ -200,19 +217,12 @@ export const ModelsList: FC = () => {
|
|||
|
||||
for (const { model_id: id, ...stats } of modelsStatsResponse) {
|
||||
const model = models.find((m) => m.model_id === id);
|
||||
model!.stats = stats;
|
||||
}
|
||||
}
|
||||
|
||||
if (pytorchModels) {
|
||||
const { deployment_stats: deploymentStatsResponse } =
|
||||
await trainedModelsApiService.getTrainedModelDeploymentStats(
|
||||
pytorchModels.map((m) => m.model_id)
|
||||
);
|
||||
|
||||
for (const { model_id: id, ...stats } of deploymentStatsResponse) {
|
||||
const model = models.find((m) => m.model_id === id);
|
||||
model!.stats!.deployment_stats = stats;
|
||||
if (model) {
|
||||
model.stats = {
|
||||
...(model.stats ?? {}),
|
||||
...stats,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -227,6 +237,39 @@ export const ModelsList: FC = () => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Updates model items with deployment stats;
|
||||
*
|
||||
* We have to fetch all deployment stats on each update,
|
||||
* because for stopped models the API returns 404 response.
|
||||
*/
|
||||
const fetchAndPopulateDeploymentStats = useCallback(async (modelItems: ModelItem[]) => {
|
||||
try {
|
||||
const { deployment_stats: deploymentStats } =
|
||||
await trainedModelsApiService.getTrainedModelDeploymentStats('*');
|
||||
|
||||
for (const deploymentStat of deploymentStats) {
|
||||
const deployedModel = modelItems.find(
|
||||
(model) => model.model_id === deploymentStat.model_id
|
||||
);
|
||||
|
||||
if (deployedModel) {
|
||||
deployedModel.stats = {
|
||||
...(deployedModel.stats ?? {}),
|
||||
deployment_stats: omit(deploymentStat, 'model_id'),
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
displayErrorToast(
|
||||
error,
|
||||
i18n.translate('xpack.ml.trainedModels.modelsList.fetchDeploymentStatsErrorMessage', {
|
||||
defaultMessage: 'Fetch deployment stats failed',
|
||||
})
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Unique inference types from models
|
||||
*/
|
||||
|
@ -361,12 +404,19 @@ export const ModelsList: FC = () => {
|
|||
description: i18n.translate('xpack.ml.inference.modelsList.startModelAllocationActionLabel', {
|
||||
defaultMessage: 'Start allocation',
|
||||
}),
|
||||
icon: 'download',
|
||||
icon: 'play',
|
||||
type: 'icon',
|
||||
isPrimary: true,
|
||||
enabled: (item) => {
|
||||
const { state } = item.stats?.deployment_stats ?? {};
|
||||
return (
|
||||
!isLoading && state !== DEPLOYMENT_STATE.STARTED && state !== DEPLOYMENT_STATE.STARTING
|
||||
);
|
||||
},
|
||||
available: (item) => item.model_type === 'pytorch',
|
||||
onClick: async (item) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await trainedModelsApiService.startModelAllocation(item.model_id);
|
||||
displaySuccessToast(
|
||||
i18n.translate('xpack.ml.trainedModels.modelsList.startSuccess', {
|
||||
|
@ -376,6 +426,7 @@ export const ModelsList: FC = () => {
|
|||
},
|
||||
})
|
||||
);
|
||||
await fetchModelsData();
|
||||
} catch (e) {
|
||||
displayErrorToast(
|
||||
e,
|
||||
|
@ -386,6 +437,7 @@ export const ModelsList: FC = () => {
|
|||
},
|
||||
})
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -400,9 +452,14 @@ export const ModelsList: FC = () => {
|
|||
type: 'icon',
|
||||
isPrimary: true,
|
||||
available: (item) => item.model_type === 'pytorch',
|
||||
enabled: (item) => !isPopulatedObject(item.pipelines),
|
||||
enabled: (item) =>
|
||||
!isLoading &&
|
||||
!isPopulatedObject(item.pipelines) &&
|
||||
isPopulatedObject(item.stats?.deployment_stats) &&
|
||||
item.stats?.deployment_stats?.state !== DEPLOYMENT_STATE.STOPPING,
|
||||
onClick: async (item) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await trainedModelsApiService.stopModelAllocation(item.model_id);
|
||||
displaySuccessToast(
|
||||
i18n.translate('xpack.ml.trainedModels.modelsList.stopSuccess', {
|
||||
|
@ -412,6 +469,8 @@ export const ModelsList: FC = () => {
|
|||
},
|
||||
})
|
||||
);
|
||||
// Need to fetch model state updates
|
||||
await fetchModelsData();
|
||||
} catch (e) {
|
||||
displayErrorToast(
|
||||
e,
|
||||
|
@ -422,6 +481,7 @@ export const ModelsList: FC = () => {
|
|||
},
|
||||
})
|
||||
);
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -521,13 +581,25 @@ export const ModelsList: FC = () => {
|
|||
),
|
||||
'data-test-subj': 'mlModelsTableColumnType',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.stateHeader', {
|
||||
defaultMessage: 'State',
|
||||
}),
|
||||
sortable: (item) => item.stats?.deployment_stats?.state,
|
||||
align: 'left',
|
||||
render: (model: ModelItem) => {
|
||||
const state = model.stats?.deployment_stats?.state;
|
||||
return state ? <EuiBadge color="hollow">{state}</EuiBadge> : null;
|
||||
},
|
||||
'data-test-subj': 'mlModelsTableColumnDeploymentState',
|
||||
},
|
||||
{
|
||||
field: ModelsTableToConfigMapping.createdAt,
|
||||
name: i18n.translate('xpack.ml.trainedModels.modelsList.createdAtHeader', {
|
||||
defaultMessage: 'Created at',
|
||||
}),
|
||||
dataType: 'date',
|
||||
render: timeFormatter,
|
||||
render: (v: number) => dateFormatter(v),
|
||||
sortable: true,
|
||||
'data-test-subj': 'mlModelsTableColumnCreatedAt',
|
||||
},
|
||||
|
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
* 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, { FC } from 'react';
|
||||
import { EuiBadge, EuiInMemoryTable, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table';
|
||||
import type {
|
||||
AllocatedModel,
|
||||
NodeDeploymentStatsResponse,
|
||||
} from '../../../../common/types/trained_models';
|
||||
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
|
||||
import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common';
|
||||
|
||||
interface AllocatedModelsProps {
|
||||
models: NodeDeploymentStatsResponse['allocated_models'];
|
||||
}
|
||||
|
||||
export const AllocatedModels: FC<AllocatedModelsProps> = ({ models }) => {
|
||||
const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES);
|
||||
const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE);
|
||||
const durationFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DURATION);
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<AllocatedModel>> = [
|
||||
{
|
||||
field: 'model_id',
|
||||
name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelNameHeader', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
width: '300px',
|
||||
sortable: true,
|
||||
truncateText: false,
|
||||
'data-test-subj': 'mlAllocatedModelsTableName',
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelSizeHeader', {
|
||||
defaultMessage: 'Size',
|
||||
}),
|
||||
width: '100px',
|
||||
truncateText: true,
|
||||
'data-test-subj': 'mlAllocatedModelsTableSize',
|
||||
render: (v: AllocatedModel) => {
|
||||
return bytesFormatter(v.model_size_bytes);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'state',
|
||||
name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelStateHeader', {
|
||||
defaultMessage: 'State',
|
||||
}),
|
||||
width: '100px',
|
||||
truncateText: false,
|
||||
'data-test-subj': 'mlAllocatedModelsTableState',
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.ml.trainedModels.nodesList.modelsList.modelAvgInferenceTimeHeader',
|
||||
{
|
||||
defaultMessage: 'Avg inference time',
|
||||
}
|
||||
),
|
||||
width: '100px',
|
||||
truncateText: false,
|
||||
'data-test-subj': 'mlAllocatedModelsTableAvgInferenceTime',
|
||||
render: (v: AllocatedModel) => {
|
||||
return v.node.average_inference_time_ms
|
||||
? durationFormatter(v.node.average_inference_time_ms)
|
||||
: '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.ml.trainedModels.nodesList.modelsList.modelInferenceCountHeader',
|
||||
{
|
||||
defaultMessage: 'Inference count',
|
||||
}
|
||||
),
|
||||
width: '100px',
|
||||
'data-test-subj': 'mlAllocatedModelsTableInferenceCount',
|
||||
render: (v: AllocatedModel) => {
|
||||
return v.node.inference_count;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelLastAccessHeader', {
|
||||
defaultMessage: 'Last access',
|
||||
}),
|
||||
width: '200px',
|
||||
'data-test-subj': 'mlAllocatedModelsTableInferenceCount',
|
||||
render: (v: AllocatedModel) => {
|
||||
return dateFormatter(v.node.last_access);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelRoutingStateHeader', {
|
||||
defaultMessage: 'Routing state',
|
||||
}),
|
||||
width: '100px',
|
||||
'data-test-subj': 'mlAllocatedModelsTableRoutingState',
|
||||
render: (v: AllocatedModel) => {
|
||||
const { routing_state: routingState, reason } = v.node.routing_state;
|
||||
|
||||
return (
|
||||
<EuiToolTip content={reason ? reason : ''}>
|
||||
<EuiBadge color={reason ? 'danger' : 'hollow'}>{routingState}</EuiBadge>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable<AllocatedModel>
|
||||
allowNeutralSort={false}
|
||||
columns={columns}
|
||||
hasActions={false}
|
||||
isExpandable={false}
|
||||
isSelectable={false}
|
||||
items={models}
|
||||
itemId={'model_id'}
|
||||
rowProps={(item) => ({
|
||||
'data-test-subj': `mlAllocatedModelTableRow row-${item.model_id}`,
|
||||
})}
|
||||
onTableChange={() => {}}
|
||||
data-test-subj={'mlNodesTable'}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -9,17 +9,15 @@ import React, { FC } from 'react';
|
|||
import {
|
||||
EuiDescriptionList,
|
||||
EuiFlexGrid,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiPanel,
|
||||
EuiSpacer,
|
||||
EuiTextColor,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { NodeItemWithStats } from './nodes_list';
|
||||
import { formatToListItems } from '../models_management/expanded_row';
|
||||
import { AllocatedModels } from './allocated_models';
|
||||
|
||||
interface ExpandedRowProps {
|
||||
item: NodeItemWithStats;
|
||||
|
@ -55,8 +53,6 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
|
|||
listItems={formatToListItems(details)}
|
||||
/>
|
||||
</EuiPanel>
|
||||
|
||||
<EuiSpacer size={'m'} />
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem>
|
||||
|
@ -76,10 +72,10 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
|
|||
listItems={formatToListItems(attributes)}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiSpacer size={'m'} />
|
||||
|
||||
{allocatedModels.length > 0 ? (
|
||||
{allocatedModels.length > 0 ? (
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiPanel>
|
||||
<EuiTitle size={'xs'}>
|
||||
<h5>
|
||||
|
@ -91,34 +87,10 @@ export const ExpandedRow: FC<ExpandedRowProps> = ({ item }) => {
|
|||
</EuiTitle>
|
||||
<EuiSpacer size={'m'} />
|
||||
|
||||
{allocatedModels.map(({ model_id: modelId, ...rest }) => {
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xxs">
|
||||
<EuiTextColor color="subdued">
|
||||
<h5>{modelId}</h5>
|
||||
</EuiTextColor>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule size={'full'} margin={'s'} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiDescriptionList
|
||||
compressed={true}
|
||||
type="column"
|
||||
listItems={formatToListItems(rest)}
|
||||
/>
|
||||
<EuiSpacer size={'s'} />
|
||||
</>
|
||||
);
|
||||
})}
|
||||
<AllocatedModels models={allocatedModels} />
|
||||
</EuiPanel>
|
||||
) : null}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexItem>
|
||||
) : null}
|
||||
</EuiFlexGrid>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -8,25 +8,26 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import {
|
||||
Chart,
|
||||
Settings,
|
||||
BarSeries,
|
||||
ScaleType,
|
||||
Axis,
|
||||
BarSeries,
|
||||
Chart,
|
||||
Position,
|
||||
ScaleType,
|
||||
SeriesColorAccessor,
|
||||
Settings,
|
||||
} from '@elastic/charts';
|
||||
import { euiPaletteGray } from '@elastic/eui';
|
||||
import { NodeDeploymentStatsResponse } from '../../../../common/types/trained_models';
|
||||
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
|
||||
import { useCurrentEuiTheme } from '../../components/color_range_legend';
|
||||
import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common';
|
||||
|
||||
interface MemoryPreviewChartProps {
|
||||
memoryOverview: NodeDeploymentStatsResponse['memory_overview'];
|
||||
}
|
||||
|
||||
export const MemoryPreviewChart: FC<MemoryPreviewChartProps> = ({ memoryOverview }) => {
|
||||
const bytesFormatter = useFieldFormatter('bytes');
|
||||
const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES);
|
||||
|
||||
const { euiTheme } = useCurrentEuiTheme();
|
||||
|
||||
|
@ -112,7 +113,7 @@ export const MemoryPreviewChart: FC<MemoryPreviewChartProps> = ({ memoryOverview
|
|||
tooltip={{
|
||||
headerFormatter: ({ value }) =>
|
||||
i18n.translate('xpack.ml.trainedModels.nodesList.memoryBreakdown', {
|
||||
defaultMessage: 'Approximate memory breakdown based on the node info',
|
||||
defaultMessage: 'Approximate memory breakdown',
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { FC, useCallback, useMemo, useState } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
EuiButtonIcon,
|
||||
EuiFlexGroup,
|
||||
|
@ -31,6 +31,8 @@ import { MemoryPreviewChart } from './memory_preview_chart';
|
|||
import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter';
|
||||
import { ListingPageUrlState } from '../../../../common/types/common';
|
||||
import { useToastNotificationService } from '../../services/toast_notification_service';
|
||||
import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common';
|
||||
import { useRefresh } from '../../routing/use_refresh';
|
||||
|
||||
export type NodeItem = NodeDeploymentStatsResponse;
|
||||
|
||||
|
@ -47,8 +49,11 @@ export const getDefaultNodesListState = (): ListingPageUrlState => ({
|
|||
|
||||
export const NodesList: FC = () => {
|
||||
const trainedModelsApiService = useTrainedModelsApiService();
|
||||
|
||||
const refresh = useRefresh();
|
||||
|
||||
const { displayErrorToast } = useToastNotificationService();
|
||||
const bytesFormatter = useFieldFormatter('bytes');
|
||||
const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES);
|
||||
const [items, setItems] = useState<NodeItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState<Record<string, JSX.Element>>(
|
||||
|
@ -179,6 +184,13 @@ export const NodesList: FC = () => {
|
|||
onRefresh: fetchNodesData,
|
||||
});
|
||||
|
||||
useEffect(
|
||||
function updateOnTimerRefresh() {
|
||||
fetchNodesData();
|
||||
},
|
||||
[refresh]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
|
|
|
@ -10,6 +10,7 @@ import React, { FC, Fragment, useMemo } from 'react';
|
|||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import {
|
||||
EuiBetaBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiPage,
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { NavigationMenu } from '../components/navigation_menu';
|
||||
import { ModelsList } from './models_management';
|
||||
import { TrainedModelsNavigationBar } from './navigation_bar';
|
||||
|
@ -44,14 +46,35 @@ export const Page: FC = () => {
|
|||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.title"
|
||||
defaultMessage="Trained Models"
|
||||
<EuiFlexGroup responsive={false} wrap={false} alignItems={'center'} gutterSize={'m'}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.trainedModels.title"
|
||||
defaultMessage="Trained Models"
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate('xpack.ml.navMenu.trainedModelsTabBetaLabel', {
|
||||
defaultMessage: 'Experimental',
|
||||
})}
|
||||
size="m"
|
||||
color="hollow"
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.ml.navMenu.trainedModelsTabBetaTooltipContent',
|
||||
{
|
||||
defaultMessage:
|
||||
"Model Management is an experimental feature and subject to change. We'd love to hear your feedback.",
|
||||
}
|
||||
)}
|
||||
tooltipPosition={'right'}
|
||||
/>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageHeaderSection>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
|
|
|
@ -5,8 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { TrainedModelsUrlState } from '../../../common/types/locator';
|
||||
import type {
|
||||
TrainedModelsNodesUrlState,
|
||||
TrainedModelsUrlState,
|
||||
} from '../../../common/types/locator';
|
||||
import { ML_PAGES } from '../../../common/constants/locator';
|
||||
import type { AppPageState, ListingPageUrlState } from '../../../common/types/common';
|
||||
import { setStateToKbnUrl } from '../../../../../../src/plugins/kibana_utils/public';
|
||||
|
||||
export function formatTrainedModelsManagementUrl(
|
||||
appBasePath: string,
|
||||
|
@ -14,3 +19,31 @@ export function formatTrainedModelsManagementUrl(
|
|||
): string {
|
||||
return `${appBasePath}/${ML_PAGES.TRAINED_MODELS_MANAGE}`;
|
||||
}
|
||||
|
||||
export function formatTrainedModelsNodesManagementUrl(
|
||||
appBasePath: string,
|
||||
mlUrlGeneratorState: TrainedModelsNodesUrlState['pageState']
|
||||
): string {
|
||||
let url = `${appBasePath}/${ML_PAGES.TRAINED_MODELS_NODES}`;
|
||||
if (mlUrlGeneratorState) {
|
||||
const { nodeId } = mlUrlGeneratorState;
|
||||
if (nodeId) {
|
||||
const nodesListState: Partial<ListingPageUrlState> = {
|
||||
queryText: `name:(${nodeId})`,
|
||||
};
|
||||
|
||||
const queryState: AppPageState<ListingPageUrlState> = {
|
||||
[ML_PAGES.TRAINED_MODELS_NODES]: nodesListState,
|
||||
};
|
||||
|
||||
url = setStateToKbnUrl<AppPageState<ListingPageUrlState>>(
|
||||
'_a',
|
||||
queryState,
|
||||
{ useHash: false, storeInHashQuery: false },
|
||||
url
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,10 @@ import {
|
|||
formatEditCalendarUrl,
|
||||
formatEditFilterUrl,
|
||||
} from './formatters';
|
||||
import { formatTrainedModelsManagementUrl } from './formatters/trained_models';
|
||||
import {
|
||||
formatTrainedModelsManagementUrl,
|
||||
formatTrainedModelsNodesManagementUrl,
|
||||
} from './formatters/trained_models';
|
||||
|
||||
export type { MlLocatorParams, MlLocator };
|
||||
|
||||
|
@ -70,6 +73,9 @@ export class MlLocatorDefinition implements LocatorDefinition<MlLocatorParams> {
|
|||
case ML_PAGES.TRAINED_MODELS_MANAGE:
|
||||
path = formatTrainedModelsManagementUrl('', params.pageState);
|
||||
break;
|
||||
case ML_PAGES.TRAINED_MODELS_NODES:
|
||||
path = formatTrainedModelsNodesManagementUrl('', params.pageState);
|
||||
break;
|
||||
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB:
|
||||
case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED:
|
||||
case ML_PAGES.DATA_VISUALIZER:
|
||||
|
|
|
@ -146,8 +146,8 @@ export function TrainedModelsTableProvider({ getService }: FtrProviderContext) {
|
|||
// 'Created at' will be different on each run,
|
||||
// so we will just assert that the value is in the expected timestamp format.
|
||||
expect(modelRow.createdAt).to.match(
|
||||
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/,
|
||||
`Expected trained model row created at time to have same format as '2019-12-05 12:28:34' (got '${modelRow.createdAt}')`
|
||||
/^\w{3}\s\d+,\s\d{4}\s@\s\d{2}:\d{2}:\d{2}\.\d{3}$/,
|
||||
`Expected trained model row created at time to have same format as 'Dec 5, 2019 @ 12:28:34.594' (got '${modelRow.createdAt}')`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue