mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[ML] Data frame analytics: Adds job deletion and creation to map view (#84299)
* wip: delete job node and update map * adds ability to delete job and update map * create job from index node * can clone job from map * reset map button * remove trained model node when deleting job * remove related model node. remove map tab when root node deleted * ensure model with no job shows up correctly * update types and naming * use urlGenerator * fix inner scrollbar * Adjust cytoscapeOptions after EUI update Co-authored-by: Robert Oskamp <robert.oskamp@elastic.co>
This commit is contained in:
parent
33c552feee
commit
93670ec81f
30 changed files with 702 additions and 338 deletions
|
@ -10,6 +10,15 @@ export const ANALYSIS_CONFIG_TYPE = {
|
|||
CLASSIFICATION: 'classification',
|
||||
} as const;
|
||||
|
||||
export const DATA_FRAME_TASK_STATE = {
|
||||
ANALYZING: 'analyzing',
|
||||
FAILED: 'failed',
|
||||
REINDEXING: 'reindexing',
|
||||
STARTED: 'started',
|
||||
STARTING: 'starting',
|
||||
STOPPED: 'stopped',
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_RESULTS_FIELD = 'ml';
|
||||
|
||||
export const JOB_MAP_NODE_TYPES = {
|
||||
|
|
|
@ -11,6 +11,7 @@ export const ML_PAGES = {
|
|||
ANOMALY_EXPLORER: 'explorer',
|
||||
SINGLE_METRIC_VIEWER: 'timeseriesexplorer',
|
||||
DATA_FRAME_ANALYTICS_JOBS_MANAGE: 'data_frame_analytics',
|
||||
DATA_FRAME_ANALYTICS_CREATE_JOB: 'data_frame_analytics/new_job',
|
||||
DATA_FRAME_ANALYTICS_MODELS_MANAGE: 'data_frame_analytics/models',
|
||||
DATA_FRAME_ANALYTICS_EXPLORATION: 'data_frame_analytics/exploration',
|
||||
DATA_FRAME_ANALYTICS_MAP: 'data_frame_analytics/map',
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import Boom from '@hapi/boom';
|
||||
import { EsErrorBody } from '../util/errors';
|
||||
import { ANALYSIS_CONFIG_TYPE } from '../constants/data_frame_analytics';
|
||||
import { DATA_FRAME_TASK_STATE } from '../constants/data_frame_analytics';
|
||||
|
||||
export interface DeleteDataFrameAnalyticsWithIndexStatus {
|
||||
success: boolean;
|
||||
|
@ -85,3 +86,54 @@ export interface DataFrameAnalyticsConfig {
|
|||
}
|
||||
|
||||
export type DataFrameAnalysisConfigType = typeof ANALYSIS_CONFIG_TYPE[keyof typeof ANALYSIS_CONFIG_TYPE];
|
||||
|
||||
export type DataFrameTaskStateType = typeof DATA_FRAME_TASK_STATE[keyof typeof DATA_FRAME_TASK_STATE];
|
||||
|
||||
interface ProgressSection {
|
||||
phase: string;
|
||||
progress_percent: number;
|
||||
}
|
||||
|
||||
export interface DataFrameAnalyticsStats {
|
||||
assignment_explanation?: string;
|
||||
id: DataFrameAnalyticsId;
|
||||
memory_usage?: {
|
||||
timestamp?: string;
|
||||
peak_usage_bytes: number;
|
||||
status: string;
|
||||
};
|
||||
node?: {
|
||||
attributes: Record<string, any>;
|
||||
ephemeral_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
transport_address: string;
|
||||
};
|
||||
progress: ProgressSection[];
|
||||
failure_reason?: string;
|
||||
state: DataFrameTaskStateType;
|
||||
}
|
||||
|
||||
export interface AnalyticsMapNodeElement {
|
||||
data: {
|
||||
id: string;
|
||||
label: string;
|
||||
type: string;
|
||||
analysisType?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AnalyticsMapEdgeElement {
|
||||
data: {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type MapElements = AnalyticsMapNodeElement | AnalyticsMapEdgeElement;
|
||||
export interface AnalyticsMapReturnType {
|
||||
elements: MapElements[];
|
||||
details: Record<string, any>; // transform, job, or index details
|
||||
error: null | any;
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ export type MlGenericUrlState = MLPageState<
|
|||
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB
|
||||
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_TYPE
|
||||
| typeof ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_SELECT_INDEX
|
||||
| typeof ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB
|
||||
| typeof ML_PAGES.OVERVIEW
|
||||
| typeof ML_PAGES.CALENDARS_MANAGE
|
||||
| typeof ML_PAGES.CALENDARS_NEW
|
||||
|
@ -158,6 +159,7 @@ export type TimeSeriesExplorerUrlState = MLPageState<
|
|||
>;
|
||||
|
||||
export interface DataFrameAnalyticsQueryState {
|
||||
analysisType?: DataFrameAnalysisConfigType;
|
||||
jobId?: JobId | JobId[];
|
||||
modelId?: string;
|
||||
groupIds?: string[];
|
||||
|
@ -165,7 +167,9 @@ export interface DataFrameAnalyticsQueryState {
|
|||
}
|
||||
|
||||
export type DataFrameAnalyticsUrlState = MLPageState<
|
||||
typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE | typeof ML_PAGES.DATA_FRAME_ANALYTICS_MAP,
|
||||
| typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE
|
||||
| typeof ML_PAGES.DATA_FRAME_ANALYTICS_MAP
|
||||
| typeof ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB,
|
||||
DataFrameAnalyticsQueryState | undefined
|
||||
>;
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ import { useMlContext } from '../../contexts/ml';
|
|||
import { DataFrameAnalyticsConfig } from '../common';
|
||||
|
||||
import { isGetDataFrameAnalyticsStatsResponseOk } from '../pages/analytics_management/services/analytics_service/get_analytics';
|
||||
import { DATA_FRAME_TASK_STATE } from '../pages/analytics_management/components/analytics_list/common';
|
||||
import { DataFrameTaskStateType } from '../pages/analytics_management/components/analytics_list/common';
|
||||
import { useTrainedModelsApiService } from '../../services/ml_api_service/trained_models';
|
||||
import { TotalFeatureImportance } from '../../../../common/types/feature_importance';
|
||||
import { getToastNotificationService } from '../../services/toast_notification_service';
|
||||
|
@ -45,7 +45,7 @@ export const useResultsViewConfig = (jobId: string) => {
|
|||
undefined
|
||||
);
|
||||
const [jobConfigErrorMessage, setJobConfigErrorMessage] = useState<undefined | string>(undefined);
|
||||
const [jobStatus, setJobStatus] = useState<DATA_FRAME_TASK_STATE | undefined>(undefined);
|
||||
const [jobStatus, setJobStatus] = useState<DataFrameTaskStateType | undefined>(undefined);
|
||||
|
||||
const [totalFeatureImportance, setTotalFeatureImportance] = useState<
|
||||
TotalFeatureImportance[] | undefined
|
||||
|
|
|
@ -29,7 +29,7 @@ import {
|
|||
DataFrameAnalyticsConfig,
|
||||
} from '../../../../common';
|
||||
import { isKeywordAndTextType } from '../../../../common/fields';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
|
||||
import { DataFrameTaskStateType } from '../../../analytics_management/components/analytics_list/common';
|
||||
import {
|
||||
isResultsSearchBoolQuery,
|
||||
isClassificationEvaluateResponse,
|
||||
|
@ -49,7 +49,7 @@ import {
|
|||
|
||||
export interface EvaluatePanelProps {
|
||||
jobConfig: DataFrameAnalyticsConfig;
|
||||
jobStatus?: DATA_FRAME_TASK_STATE;
|
||||
jobStatus?: DataFrameTaskStateType;
|
||||
searchQuery: ResultsSearchQuery;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '../../../../common';
|
||||
import { ResultsSearchQuery } from '../../../../common/analytics';
|
||||
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
|
||||
import { DataFrameTaskStateType } from '../../../analytics_management/components/analytics_list/common';
|
||||
|
||||
import { ExpandableSectionAnalytics } from '../expandable_section';
|
||||
import { ExplorationResultsTable } from '../exploration_results_table';
|
||||
|
@ -48,7 +48,7 @@ const filters = {
|
|||
|
||||
export interface EvaluatePanelProps {
|
||||
jobConfig: DataFrameAnalyticsConfig;
|
||||
jobStatus?: DATA_FRAME_TASK_STATE;
|
||||
jobStatus?: DataFrameTaskStateType;
|
||||
searchQuery: ResultsSearchQuery;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { useMlKibana } from '../../../../../contexts/kibana';
|
|||
import { DataFrameAnalyticsConfig } from '../../../../common';
|
||||
import { ResultsSearchQuery } from '../../../../common/analytics';
|
||||
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
|
||||
import { DataFrameTaskStateType } from '../../../analytics_management/components/analytics_list/common';
|
||||
|
||||
import { ExpandableSectionResults } from '../expandable_section';
|
||||
|
||||
|
@ -23,7 +23,7 @@ import { useExplorationResults } from './use_exploration_results';
|
|||
interface Props {
|
||||
indexPattern: IndexPattern;
|
||||
jobConfig: DataFrameAnalyticsConfig;
|
||||
jobStatus?: DATA_FRAME_TASK_STATE;
|
||||
jobStatus?: DataFrameTaskStateType;
|
||||
needsDestIndexPattern: boolean;
|
||||
searchQuery: ResultsSearchQuery;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
Eval,
|
||||
DataFrameAnalyticsConfig,
|
||||
} from '../../../../common';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common';
|
||||
import { DataFrameTaskStateType } from '../../../analytics_management/components/analytics_list/common';
|
||||
import {
|
||||
isResultsSearchBoolQuery,
|
||||
isRegressionEvaluateResponse,
|
||||
|
@ -41,7 +41,7 @@ import { EvaluateStat } from './evaluate_stat';
|
|||
|
||||
interface Props {
|
||||
jobConfig: DataFrameAnalyticsConfig;
|
||||
jobStatus?: DATA_FRAME_TASK_STATE;
|
||||
jobStatus?: DataFrameTaskStateType;
|
||||
searchQuery: SavedSearchQuery;
|
||||
}
|
||||
|
||||
|
|
|
@ -343,7 +343,7 @@ export const useNavigateToWizardWithClonedJob = () => {
|
|||
|
||||
const savedObjectsClient = savedObjects.client;
|
||||
|
||||
return async (item: DataFrameAnalyticsListRow) => {
|
||||
return async (item: Pick<DataFrameAnalyticsListRow, 'config' | 'stats'>) => {
|
||||
const sourceIndex = Array.isArray(item.config.source.index)
|
||||
? item.config.source.index.join(',')
|
||||
: item.config.source.index;
|
||||
|
|
|
@ -29,11 +29,13 @@ import {
|
|||
|
||||
import { deleteActionNameText, DeleteActionName } from './delete_action_name';
|
||||
|
||||
type DataFrameAnalyticsListRowEssentials = Pick<DataFrameAnalyticsListRow, 'config' | 'stats'>;
|
||||
export type DeleteAction = ReturnType<typeof useDeleteAction>;
|
||||
export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
|
||||
const [item, setItem] = useState<DataFrameAnalyticsListRow>();
|
||||
const [item, setItem] = useState<DataFrameAnalyticsListRowEssentials>();
|
||||
|
||||
const [isModalVisible, setModalVisible] = useState(false);
|
||||
const [deleteItem, setDeleteItem] = useState(false);
|
||||
const [deleteTargetIndex, setDeleteTargetIndex] = useState<boolean>(true);
|
||||
const [deleteIndexPattern, setDeleteIndexPattern] = useState<boolean>(true);
|
||||
const [userCanDeleteIndex, setUserCanDeleteIndex] = useState<boolean>(false);
|
||||
|
@ -111,25 +113,27 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
|
|||
|
||||
const closeModal = () => setModalVisible(false);
|
||||
const deleteAndCloseModal = () => {
|
||||
setDeleteItem(true);
|
||||
setModalVisible(false);
|
||||
|
||||
if (item !== undefined) {
|
||||
if ((userCanDeleteIndex && deleteTargetIndex) || (userCanDeleteIndex && deleteIndexPattern)) {
|
||||
deleteAnalyticsAndDestIndex(
|
||||
item,
|
||||
item.config,
|
||||
item.stats,
|
||||
deleteTargetIndex,
|
||||
indexPatternExists && deleteIndexPattern,
|
||||
toastNotificationService
|
||||
);
|
||||
} else {
|
||||
deleteAnalytics(item, toastNotificationService);
|
||||
deleteAnalytics(item.config, item.stats, toastNotificationService);
|
||||
}
|
||||
}
|
||||
};
|
||||
const toggleDeleteIndex = () => setDeleteTargetIndex(!deleteTargetIndex);
|
||||
const toggleDeleteIndexPattern = () => setDeleteIndexPattern(!deleteIndexPattern);
|
||||
|
||||
const openModal = (newItem: DataFrameAnalyticsListRow) => {
|
||||
const openModal = (newItem: DataFrameAnalyticsListRowEssentials) => {
|
||||
setItem(newItem);
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
@ -159,6 +163,7 @@ export const useDeleteAction = (canDeleteDataFrameAnalytics: boolean) => {
|
|||
deleteAndCloseModal,
|
||||
deleteTargetIndex,
|
||||
deleteIndexPattern,
|
||||
deleteItem,
|
||||
indexPatternExists,
|
||||
isModalVisible,
|
||||
item,
|
||||
|
|
|
@ -6,11 +6,18 @@
|
|||
|
||||
import { EuiTableActionsColumnType, Query, Ast } from '@elastic/eui';
|
||||
|
||||
import { DATA_FRAME_TASK_STATE } from './data_frame_task_state';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../../../../../common/constants/data_frame_analytics';
|
||||
import { DataFrameTaskStateType } from '../../../../../../../common/types/data_frame_analytics';
|
||||
export { DATA_FRAME_TASK_STATE };
|
||||
export { DataFrameTaskStateType };
|
||||
|
||||
import { DataFrameAnalyticsId, DataFrameAnalyticsConfig } from '../../../../common';
|
||||
import { DataFrameAnalysisConfigType } from '../../../../../../../common/types/data_frame_analytics';
|
||||
import {
|
||||
DataFrameAnalysisConfigType,
|
||||
DataFrameAnalyticsStats,
|
||||
} from '../../../../../../../common/types/data_frame_analytics';
|
||||
|
||||
export { DataFrameAnalyticsStats } from '../../../../../../../common/types/data_frame_analytics';
|
||||
|
||||
export enum DATA_FRAME_MODE {
|
||||
BATCH = 'batch',
|
||||
|
@ -25,36 +32,11 @@ export type TermClause = ExtractClauseType<typeof Ast['Term']['isInstance']>;
|
|||
export type FieldClause = ExtractClauseType<typeof Ast['Field']['isInstance']>;
|
||||
export type Value = Parameters<typeof Ast['Term']['must']>[0];
|
||||
|
||||
interface ProgressSection {
|
||||
phase: string;
|
||||
progress_percent: number;
|
||||
}
|
||||
|
||||
export interface DataFrameAnalyticsStats {
|
||||
assignment_explanation?: string;
|
||||
id: DataFrameAnalyticsId;
|
||||
memory_usage?: {
|
||||
timestamp?: string;
|
||||
peak_usage_bytes: number;
|
||||
status: string;
|
||||
};
|
||||
node?: {
|
||||
attributes: Record<string, any>;
|
||||
ephemeral_id: string;
|
||||
id: string;
|
||||
name: string;
|
||||
transport_address: string;
|
||||
};
|
||||
progress: ProgressSection[];
|
||||
failure_reason?: string;
|
||||
state: DATA_FRAME_TASK_STATE;
|
||||
}
|
||||
|
||||
export function isDataFrameAnalyticsFailed(state: DATA_FRAME_TASK_STATE) {
|
||||
export function isDataFrameAnalyticsFailed(state: DataFrameTaskStateType) {
|
||||
return state === DATA_FRAME_TASK_STATE.FAILED;
|
||||
}
|
||||
|
||||
export function isDataFrameAnalyticsRunning(state: DATA_FRAME_TASK_STATE) {
|
||||
export function isDataFrameAnalyticsRunning(state: DataFrameTaskStateType) {
|
||||
return (
|
||||
state === DATA_FRAME_TASK_STATE.ANALYZING ||
|
||||
state === DATA_FRAME_TASK_STATE.REINDEXING ||
|
||||
|
@ -63,7 +45,7 @@ export function isDataFrameAnalyticsRunning(state: DATA_FRAME_TASK_STATE) {
|
|||
);
|
||||
}
|
||||
|
||||
export function isDataFrameAnalyticsStopped(state: DATA_FRAME_TASK_STATE) {
|
||||
export function isDataFrameAnalyticsStopped(state: DataFrameTaskStateType) {
|
||||
return state === DATA_FRAME_TASK_STATE.STOPPED;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
// DATA_FRAME_TASK_STATE is used by x-pack functional test setup/config
|
||||
// and that config cannot import from './common.ts' because it has imports dependant on a browser-environment
|
||||
|
||||
export enum DATA_FRAME_TASK_STATE {
|
||||
ANALYZING = 'analyzing',
|
||||
FAILED = 'failed',
|
||||
REINDEXING = 'reindexing',
|
||||
STARTED = 'started',
|
||||
STARTING = 'starting',
|
||||
STOPPED = 'stopped',
|
||||
}
|
|
@ -92,13 +92,15 @@ export const Page: FC = () => {
|
|||
<EuiPageHeaderSection>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s">
|
||||
{selectedTabId !== 'map' && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<RefreshAnalyticsListButton />
|
||||
</EuiFlexItem>
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RefreshAnalyticsListButton />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper />
|
||||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DatePickerWrapper />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
|
|
|
@ -14,18 +14,19 @@ import {
|
|||
} from '../../components/analytics_list/common';
|
||||
|
||||
export const deleteAnalytics = async (
|
||||
d: DataFrameAnalyticsListRow,
|
||||
analyticsConfig: DataFrameAnalyticsListRow['config'],
|
||||
analyticsStats: DataFrameAnalyticsListRow['stats'],
|
||||
toastNotificationService: ToastNotificationService
|
||||
) => {
|
||||
try {
|
||||
if (isDataFrameAnalyticsFailed(d.stats.state)) {
|
||||
await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true);
|
||||
if (isDataFrameAnalyticsFailed(analyticsStats.state)) {
|
||||
await ml.dataFrameAnalytics.stopDataFrameAnalytics(analyticsConfig.id, true);
|
||||
}
|
||||
await ml.dataFrameAnalytics.deleteDataFrameAnalytics(d.config.id);
|
||||
await ml.dataFrameAnalytics.deleteDataFrameAnalytics(analyticsConfig.id);
|
||||
toastNotificationService.displaySuccessToast(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', {
|
||||
defaultMessage: 'Request to delete data frame analytics job {analyticsId} acknowledged.',
|
||||
values: { analyticsId: d.config.id },
|
||||
values: { analyticsId: analyticsConfig.id },
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
|
@ -33,7 +34,7 @@ export const deleteAnalytics = async (
|
|||
e,
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', {
|
||||
defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}',
|
||||
values: { analyticsId: d.config.id },
|
||||
values: { analyticsId: analyticsConfig.id },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -41,20 +42,21 @@ export const deleteAnalytics = async (
|
|||
};
|
||||
|
||||
export const deleteAnalyticsAndDestIndex = async (
|
||||
d: DataFrameAnalyticsListRow,
|
||||
analyticsConfig: DataFrameAnalyticsListRow['config'],
|
||||
analyticsStats: DataFrameAnalyticsListRow['stats'],
|
||||
deleteDestIndex: boolean,
|
||||
deleteDestIndexPattern: boolean,
|
||||
toastNotificationService: ToastNotificationService
|
||||
) => {
|
||||
const destinationIndex = Array.isArray(d.config.dest.index)
|
||||
? d.config.dest.index[0]
|
||||
: d.config.dest.index;
|
||||
const destinationIndex = Array.isArray(analyticsConfig.dest.index)
|
||||
? analyticsConfig.dest.index[0]
|
||||
: analyticsConfig.dest.index;
|
||||
try {
|
||||
if (isDataFrameAnalyticsFailed(d.stats.state)) {
|
||||
await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true);
|
||||
if (isDataFrameAnalyticsFailed(analyticsStats.state)) {
|
||||
await ml.dataFrameAnalytics.stopDataFrameAnalytics(analyticsConfig.id, true);
|
||||
}
|
||||
const status = await ml.dataFrameAnalytics.deleteDataFrameAnalyticsAndDestIndex(
|
||||
d.config.id,
|
||||
analyticsConfig.id,
|
||||
deleteDestIndex,
|
||||
deleteDestIndexPattern
|
||||
);
|
||||
|
@ -62,7 +64,7 @@ export const deleteAnalyticsAndDestIndex = async (
|
|||
toastNotificationService.displaySuccessToast(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage', {
|
||||
defaultMessage: 'Request to delete data frame analytics job {analyticsId} acknowledged.',
|
||||
values: { analyticsId: d.config.id },
|
||||
values: { analyticsId: analyticsConfig.id },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -71,7 +73,7 @@ export const deleteAnalyticsAndDestIndex = async (
|
|||
status.analyticsJobDeleted.error,
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', {
|
||||
defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}',
|
||||
values: { analyticsId: d.config.id },
|
||||
values: { analyticsId: analyticsConfig.id },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -122,7 +124,7 @@ export const deleteAnalyticsAndDestIndex = async (
|
|||
e,
|
||||
i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage', {
|
||||
defaultMessage: 'An error occurred deleting the data frame analytics job {analyticsId}',
|
||||
values: { analyticsId: d.config.id },
|
||||
values: { analyticsId: analyticsConfig.id },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
height: $euiSizeM;
|
||||
width: $euiSizeM;
|
||||
background-color: $euiColorGhost;
|
||||
border: 1px solid $euiColorVis2;
|
||||
border: $euiBorderWidthThick solid $euiColorVis2;
|
||||
transform: rotate(45deg);
|
||||
display: 'inline-block';
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
|||
height: $euiSizeM;
|
||||
width: $euiSizeM;
|
||||
background-color: $euiColorGhost;
|
||||
border: 1px solid $euiColorVis1;
|
||||
border: $euiBorderWidthThick solid $euiColorVis1;
|
||||
display: 'inline-block';
|
||||
}
|
||||
|
||||
|
@ -23,17 +23,17 @@
|
|||
height: $euiSizeM;
|
||||
width: $euiSizeM;
|
||||
background-color: $euiColorGhost;
|
||||
border: 1px solid $euiColorVis0;
|
||||
border-radius: $euiBorderRadius;
|
||||
border: $euiBorderWidthThick solid $euiColorVis0;
|
||||
border-radius: 50%;
|
||||
display: 'inline-block';
|
||||
}
|
||||
|
||||
.mlJobMapLegend__trainedModel {
|
||||
height: $euiSizeM;
|
||||
width: $euiSizeM;
|
||||
background-color: $euiColorGhost;
|
||||
border: $euiBorderThin;
|
||||
border-radius: $euiBorderRadius;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: $euiSizeS solid $euiColorGhost;
|
||||
border-right: $euiSizeS solid $euiColorGhost;
|
||||
border-bottom: $euiSizeM solid $euiColorVis3;
|
||||
display: 'inline-block';
|
||||
}
|
||||
|
||||
|
|
|
@ -7,10 +7,13 @@
|
|||
import React, { FC, useEffect, useState, useContext, useCallback } from 'react';
|
||||
import cytoscape from 'cytoscape';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
EuiButtonEmpty,
|
||||
EuiButton,
|
||||
EuiCodeBlock,
|
||||
EuiContextMenuItem,
|
||||
EuiContextMenuPanel,
|
||||
EuiDescriptionList,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -18,6 +21,7 @@ import {
|
|||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiFlyoutBody,
|
||||
EuiPopover,
|
||||
EuiPortal,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
@ -25,13 +29,26 @@ import { EuiDescriptionListProps } from '@elastic/eui/src/components/description
|
|||
import { CytoscapeContext } from './cytoscape';
|
||||
import { formatHumanReadableDateTimeSeconds } from '../../../../../../common/util/date_utils';
|
||||
import { JOB_MAP_NODE_TYPES } from '../../../../../../common/constants/data_frame_analytics';
|
||||
// import { DeleteButton } from './delete_button'; // TODO: add delete functionality in followup
|
||||
import { ML_PAGES } from '../../../../../../common/constants/ml_url_generator';
|
||||
import { checkPermission } from '../../../../capabilities/check_capabilities';
|
||||
import {
|
||||
useMlUrlGenerator,
|
||||
useNotifications,
|
||||
useNavigateToPath,
|
||||
} from '../../../../contexts/kibana';
|
||||
import { getIndexPatternIdFromName } from '../../../../util/index_utils';
|
||||
import { useNavigateToWizardWithClonedJob } from '../../analytics_management/components/action_clone/clone_action_name';
|
||||
import {
|
||||
useDeleteAction,
|
||||
DeleteActionModal,
|
||||
} from '../../analytics_management/components/action_delete';
|
||||
|
||||
interface Props {
|
||||
analyticsId?: string;
|
||||
modelId?: string;
|
||||
details: any;
|
||||
getNodeData: any;
|
||||
modelId?: string;
|
||||
updateElements: (nodeId: string, nodeLabel: string, destIndexNode?: string) => void;
|
||||
}
|
||||
|
||||
function getListItems(details: object): EuiDescriptionListProps['listItems'] {
|
||||
|
@ -57,9 +74,24 @@ function getListItems(details: object): EuiDescriptionListProps['listItems'] {
|
|||
});
|
||||
}
|
||||
|
||||
export const Controls: FC<Props> = ({ analyticsId, modelId, details, getNodeData }) => {
|
||||
export const Controls: FC<Props> = ({
|
||||
analyticsId,
|
||||
details,
|
||||
getNodeData,
|
||||
modelId,
|
||||
updateElements,
|
||||
}) => {
|
||||
const [showFlyout, setShowFlyout] = useState<boolean>(false);
|
||||
const [selectedNode, setSelectedNode] = useState<cytoscape.NodeSingular | undefined>();
|
||||
const [isPopoverOpen, setPopover] = useState(false);
|
||||
|
||||
const canDeleteDataFrameAnalytics: boolean = checkPermission('canDeleteDataFrameAnalytics');
|
||||
const deleteAction = useDeleteAction(canDeleteDataFrameAnalytics);
|
||||
const { deleteItem, deleteTargetIndex, isModalVisible, openModal } = deleteAction;
|
||||
const { toasts } = useNotifications();
|
||||
const mlUrlGenerator = useMlUrlGenerator();
|
||||
const navigateToPath = useNavigateToPath();
|
||||
const navigateToWizardWithClonedJob = useNavigateToWizardWithClonedJob();
|
||||
|
||||
const cy = useContext(CytoscapeContext);
|
||||
const deselect = useCallback(() => {
|
||||
|
@ -74,6 +106,39 @@ export const Controls: FC<Props> = ({ analyticsId, modelId, details, getNodeData
|
|||
const nodeLabel = selectedNode?.data('label');
|
||||
const nodeType = selectedNode?.data('type');
|
||||
|
||||
const onCreateJobClick = useCallback(async () => {
|
||||
const indexId = getIndexPatternIdFromName(nodeLabel);
|
||||
|
||||
if (indexId) {
|
||||
const path = await mlUrlGenerator.createUrl({
|
||||
page: ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB,
|
||||
pageState: { index: indexId },
|
||||
});
|
||||
|
||||
await navigateToPath(path);
|
||||
} else {
|
||||
toasts.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.indexPatternMissingMessage', {
|
||||
defaultMessage:
|
||||
'To create a job from this index please create an index pattern for {indexTitle}.',
|
||||
values: { indexTitle: nodeLabel },
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [nodeLabel]);
|
||||
|
||||
const onCloneJobClick = useCallback(async () => {
|
||||
navigateToWizardWithClonedJob({ config: details[nodeId], stats: details[nodeId]?.stats });
|
||||
}, [nodeId]);
|
||||
|
||||
const onActionsButtonClick = () => {
|
||||
setPopover(!isPopoverOpen);
|
||||
};
|
||||
|
||||
const closePopover = () => {
|
||||
setPopover(false);
|
||||
};
|
||||
|
||||
// Set up Cytoscape event handlers
|
||||
useEffect(() => {
|
||||
const selectHandler: cytoscape.EventHandler = (event) => {
|
||||
|
@ -94,27 +159,93 @@ export const Controls: FC<Props> = ({ analyticsId, modelId, details, getNodeData
|
|||
};
|
||||
}, [cy, deselect]);
|
||||
|
||||
useEffect(
|
||||
function updateElementsOnClose() {
|
||||
if (isModalVisible === false && deleteItem === true) {
|
||||
let destIndexNode;
|
||||
if (deleteTargetIndex === true) {
|
||||
const jobDetails = details[nodeId];
|
||||
const destIndex = jobDetails.dest.index;
|
||||
destIndexNode = `${destIndex}-${JOB_MAP_NODE_TYPES.INDEX}`;
|
||||
}
|
||||
updateElements(nodeId, nodeLabel, destIndexNode);
|
||||
setShowFlyout(false);
|
||||
}
|
||||
},
|
||||
[isModalVisible, deleteItem]
|
||||
);
|
||||
|
||||
if (showFlyout === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nodeDataButton =
|
||||
analyticsId !== nodeLabel &&
|
||||
const button = (
|
||||
<EuiButton size="s" iconType="arrowDown" iconSide="right" onClick={onActionsButtonClick}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsMap.flyout.nodeActionsButton"
|
||||
defaultMessage="Node actions"
|
||||
/>
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
const items = [
|
||||
...(nodeType === JOB_MAP_NODE_TYPES.ANALYTICS
|
||||
? [
|
||||
<EuiContextMenuItem
|
||||
key={`${nodeId}-delete`}
|
||||
icon="trash"
|
||||
onClick={() => {
|
||||
openModal({ config: details[nodeId], stats: details[nodeId]?.stats });
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsMap.flyout.deleteJobButton"
|
||||
defaultMessage="Delete job"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem key={`${nodeId}-clone`} icon="copy" onClick={onCloneJobClick}>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsMap.flyout.cloneJobButton"
|
||||
defaultMessage="Clone job"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
...(nodeType === JOB_MAP_NODE_TYPES.INDEX
|
||||
? [
|
||||
<EuiContextMenuItem
|
||||
key={`${nodeId}-create`}
|
||||
icon="plusInCircle"
|
||||
onClick={onCreateJobClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsMap.flyout.createJobButton"
|
||||
defaultMessage="Create job from this index"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
...(analyticsId !== nodeLabel &&
|
||||
modelId !== nodeLabel &&
|
||||
(nodeType === JOB_MAP_NODE_TYPES.ANALYTICS || nodeType === JOB_MAP_NODE_TYPES.INDEX) ? (
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
getNodeData({ id: nodeLabel, type: nodeType });
|
||||
setShowFlyout(false);
|
||||
}}
|
||||
iconType="branch"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsMap.flyout.fetchRelatedNodesButton"
|
||||
defaultMessage="Fetch related nodes"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
) : null;
|
||||
(nodeType === JOB_MAP_NODE_TYPES.ANALYTICS || nodeType === JOB_MAP_NODE_TYPES.INDEX)
|
||||
? [
|
||||
<EuiContextMenuItem
|
||||
key={`${nodeId}-fetch-related`}
|
||||
icon="branch"
|
||||
onClick={() => {
|
||||
getNodeData({ id: nodeLabel, type: nodeType });
|
||||
setShowFlyout(false);
|
||||
setPopover(false);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsMap.flyout.fetchRelatedNodesButton"
|
||||
defaultMessage="Fetch related nodes"
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
|
@ -155,14 +286,20 @@ export const Controls: FC<Props> = ({ analyticsId, modelId, details, getNodeData
|
|||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>{nodeDataButton}</EuiFlexItem>
|
||||
{/* <EuiFlexItem grow={false}>
|
||||
<DeleteButton id={nodeLabel} type={nodeType} />
|
||||
</EuiFlexItem> */}
|
||||
</EuiFlexGroup>
|
||||
{nodeType !== JOB_MAP_NODE_TYPES.TRAINED_MODEL && (
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={closePopover}
|
||||
panelPaddingSize="s"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel items={items} />
|
||||
</EuiPopover>
|
||||
)}
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
{isModalVisible && <DeleteActionModal {...deleteAction} />}
|
||||
</EuiPortal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -26,6 +26,8 @@ interface CytoscapeProps {
|
|||
children?: ReactNode;
|
||||
elements: cytoscape.ElementDefinition[];
|
||||
height: number;
|
||||
itemsDeleted: boolean;
|
||||
resetCy: boolean;
|
||||
style?: CSSProperties;
|
||||
width: number;
|
||||
}
|
||||
|
@ -63,7 +65,15 @@ function getLayoutOptions(width: number, height: number) {
|
|||
};
|
||||
}
|
||||
|
||||
export function Cytoscape({ children, elements, height, style, width }: CytoscapeProps) {
|
||||
export function Cytoscape({
|
||||
children,
|
||||
elements,
|
||||
height,
|
||||
itemsDeleted,
|
||||
resetCy,
|
||||
style,
|
||||
width,
|
||||
}: CytoscapeProps) {
|
||||
const [ref, cy] = useCytoscape({
|
||||
...cytoscapeOptions,
|
||||
elements,
|
||||
|
@ -76,7 +86,8 @@ export function Cytoscape({ children, elements, height, style, width }: Cytoscap
|
|||
const dataHandler = useCallback<cytoscape.EventHandler>(
|
||||
(event) => {
|
||||
if (cy && height > 0) {
|
||||
cy.layout(getLayoutOptions(width, height)).run();
|
||||
// temporary workaround for single 'row' maps showing up outside of the graph bounds
|
||||
setTimeout(() => cy.layout(getLayoutOptions(width, height)).run(), 150);
|
||||
}
|
||||
},
|
||||
[cy, height, width]
|
||||
|
@ -98,11 +109,24 @@ export function Cytoscape({ children, elements, height, style, width }: Cytoscap
|
|||
// Trigger a custom "data" event when data changes
|
||||
useEffect(() => {
|
||||
if (cy) {
|
||||
cy.add(elements);
|
||||
if (itemsDeleted === false) {
|
||||
cy.add(elements);
|
||||
} else {
|
||||
cy.elements().remove();
|
||||
cy.add(elements);
|
||||
}
|
||||
|
||||
cy.trigger('data');
|
||||
}
|
||||
}, [cy, elements]);
|
||||
|
||||
// Reset the graph to original zoom and pan
|
||||
useEffect(() => {
|
||||
if (cy) {
|
||||
cy.reset();
|
||||
}
|
||||
}, [cy, resetCy]);
|
||||
|
||||
return (
|
||||
<CytoscapeContext.Provider value={cy}>
|
||||
<div ref={ref} style={divStyle}>
|
||||
|
|
|
@ -20,6 +20,7 @@ const MAP_SHAPES = {
|
|||
ELLIPSE: 'ellipse',
|
||||
RECTANGLE: 'rectangle',
|
||||
DIAMOND: 'diamond',
|
||||
TRIANGLE: 'triangle',
|
||||
} as const;
|
||||
type MapShapes = typeof MAP_SHAPES[keyof typeof MAP_SHAPES];
|
||||
|
||||
|
@ -32,6 +33,8 @@ function shapeForNode(el: cytoscape.NodeSingular): MapShapes {
|
|||
return MAP_SHAPES.RECTANGLE;
|
||||
case JOB_MAP_NODE_TYPES.INDEX:
|
||||
return MAP_SHAPES.DIAMOND;
|
||||
case JOB_MAP_NODE_TYPES.TRAINED_MODEL:
|
||||
return MAP_SHAPES.TRIANGLE;
|
||||
default:
|
||||
return MAP_SHAPES.ELLIPSE;
|
||||
}
|
||||
|
@ -66,6 +69,8 @@ function borderColorForNode(el: cytoscape.NodeSingular) {
|
|||
return theme.euiColorVis1;
|
||||
case JOB_MAP_NODE_TYPES.INDEX:
|
||||
return theme.euiColorVis2;
|
||||
case JOB_MAP_NODE_TYPES.TRAINED_MODEL:
|
||||
return theme.euiColorVis3;
|
||||
default:
|
||||
return theme.euiColorMediumShade;
|
||||
}
|
||||
|
@ -88,7 +93,8 @@ export const cytoscapeOptions: cytoscape.CytoscapeOptions = {
|
|||
'border-style': 'solid',
|
||||
// @ts-ignore
|
||||
'background-image': (el: cytoscape.NodeSingular) => iconForNode(el),
|
||||
'border-width': (el: cytoscape.NodeSingular) => (el.selected() ? 2 : 1),
|
||||
'border-width': (el: cytoscape.NodeSingular) => (el.selected() ? 4 : 3),
|
||||
// @ts-ignore
|
||||
color: theme.euiTextColors.default,
|
||||
'font-family': 'Inter UI, Segoe UI, Helvetica, Arial, sans-serif',
|
||||
'font-size': theme.euiFontSizeXS,
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ml } from '../../../../services/ml_api_service';
|
||||
import { getToastNotifications } from '../../../../util/dependency_cache';
|
||||
import {
|
||||
JOB_MAP_NODE_TYPES,
|
||||
JobMapNodeTypes,
|
||||
} from '../../../../../../common/constants/data_frame_analytics';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
type: JobMapNodeTypes;
|
||||
}
|
||||
|
||||
export const DeleteButton: FC<Props> = ({ id, type }) => {
|
||||
const toastNotifications = getToastNotifications();
|
||||
|
||||
const onDelete = async () => {
|
||||
try {
|
||||
// if (isDataFrameAnalyticsFailed(d.stats.state)) {
|
||||
// await ml.dataFrameAnalytics.stopDataFrameAnalytics(d.config.id, true);
|
||||
// }
|
||||
await ml.dataFrameAnalytics.deleteDataFrameAnalytics(id);
|
||||
toastNotifications.addSuccess(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.deleteAnalyticsSuccessMessage', {
|
||||
defaultMessage: 'Request to delete data frame analytics {id} acknowledged.',
|
||||
values: { id },
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toastNotifications.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.deleteAnalyticsErrorMessage', {
|
||||
defaultMessage: 'An error occurred deleting the data frame analytics {id}: {error}',
|
||||
values: { id, error: JSON.stringify(e) },
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (type !== JOB_MAP_NODE_TYPES.ANALYTICS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiButton onClick={onDelete} iconType="trash" color="danger" size="s">
|
||||
{i18n.translate('xpack.ml.dataframe.analyticsMap.flyout.deleteJobButton', {
|
||||
defaultMessage: 'Delete job',
|
||||
})}
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
|
@ -7,15 +7,15 @@
|
|||
import React, { FC, useEffect, useState } from 'react';
|
||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import cytoscape from 'cytoscape';
|
||||
import { uniqWith, isEqual } from 'lodash';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui';
|
||||
import { Cytoscape, Controls, JobMapLegend } from './components';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { useMlKibana } from '../../../contexts/kibana';
|
||||
import { useRefDimensions } from './components/use_ref_dimensions';
|
||||
import { useMlKibana, useMlUrlGenerator } from '../../../contexts/kibana';
|
||||
import { JOB_MAP_NODE_TYPES } from '../../../../../common/constants/data_frame_analytics';
|
||||
import { ML_PAGES } from '../../../../../common/constants/ml_url_generator';
|
||||
import { useRefDimensions } from './components/use_ref_dimensions';
|
||||
import { useFetchAnalyticsMapData } from './use_fetch_analytics_map_data';
|
||||
import { JobMapTitle } from './job_map_title';
|
||||
|
||||
const cytoscapeDivStyle = {
|
||||
background: `linear-gradient(
|
||||
|
@ -37,104 +37,84 @@ ${theme.euiColorLightShade}`,
|
|||
marginTop: 0,
|
||||
};
|
||||
|
||||
export const JobMapTitle: React.FC<{ analyticsId?: string; modelId?: string }> = ({
|
||||
analyticsId,
|
||||
modelId,
|
||||
}) => (
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
{analyticsId
|
||||
? i18n.translate('xpack.ml.dataframe.analyticsMap.analyticsIdTitle', {
|
||||
defaultMessage: 'Map for analytics ID {analyticsId}',
|
||||
values: { analyticsId },
|
||||
})
|
||||
: i18n.translate('xpack.ml.dataframe.analyticsMap.modelIdTitle', {
|
||||
defaultMessage: 'Map for trained model ID {modelId}',
|
||||
values: { modelId },
|
||||
})}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
);
|
||||
|
||||
interface GetDataObjectParameter {
|
||||
id: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
analyticsId?: string;
|
||||
modelId?: string;
|
||||
}
|
||||
|
||||
export const JobMap: FC<Props> = ({ analyticsId, modelId }) => {
|
||||
const [elements, setElements] = useState<cytoscape.ElementDefinition[]>([]);
|
||||
const [nodeDetails, setNodeDetails] = useState({});
|
||||
const [error, setError] = useState(undefined);
|
||||
// itemsDeleted will reset to false when Controls component calls updateElements to remove nodes deleted from map
|
||||
const [itemsDeleted, setItemsDeleted] = useState<boolean>(false);
|
||||
const [resetCyToggle, setResetCyToggle] = useState<boolean>(false);
|
||||
const {
|
||||
elements,
|
||||
error,
|
||||
fetchAndSetElementsWrapper,
|
||||
isLoading,
|
||||
message,
|
||||
nodeDetails,
|
||||
setElements,
|
||||
setError,
|
||||
} = useFetchAnalyticsMapData();
|
||||
|
||||
const {
|
||||
services: { notifications },
|
||||
services: {
|
||||
notifications,
|
||||
application: { navigateToUrl },
|
||||
},
|
||||
} = useMlKibana();
|
||||
const urlGenerator = useMlUrlGenerator();
|
||||
|
||||
const getDataWrapper = async (params?: GetDataObjectParameter) => {
|
||||
const { id, type } = params ?? {};
|
||||
const treatAsRoot = id !== undefined;
|
||||
let idToUse: string;
|
||||
|
||||
if (id !== undefined) {
|
||||
idToUse = id;
|
||||
} else if (modelId !== undefined) {
|
||||
idToUse = modelId;
|
||||
} else {
|
||||
idToUse = analyticsId as string;
|
||||
}
|
||||
|
||||
await getData(
|
||||
idToUse,
|
||||
treatAsRoot,
|
||||
modelId !== undefined && treatAsRoot === false ? JOB_MAP_NODE_TYPES.TRAINED_MODEL : type
|
||||
);
|
||||
const redirectToAnalyticsManagementPage = async () => {
|
||||
const url = await urlGenerator.createUrl({ page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE });
|
||||
await navigateToUrl(url);
|
||||
};
|
||||
|
||||
const getData = async (idToUse: string, treatAsRoot: boolean, type?: string) => {
|
||||
// Pass in treatAsRoot flag - endpoint will take job or index to grab jobs created from it
|
||||
// TODO: update analyticsMap return type here
|
||||
const analyticsMap: any = await ml.dataFrameAnalytics.getDataFrameAnalyticsMap(
|
||||
idToUse,
|
||||
treatAsRoot,
|
||||
type
|
||||
);
|
||||
const updateElements = (nodeId: string, nodeLabel: string, destIndexNode?: string) => {
|
||||
// If removing the root job just go back to the jobs list
|
||||
if (nodeLabel === analyticsId) {
|
||||
redirectToAnalyticsManagementPage();
|
||||
} else {
|
||||
// Remove job element
|
||||
const filteredElements = elements.filter((e) => {
|
||||
// Filter out job node and related edges, including trained model node.
|
||||
let isNotDeletedNodeOrRelated =
|
||||
e.data.id !== nodeId && e.data.target !== nodeId && e.data.source !== nodeId;
|
||||
|
||||
const { elements: nodeElements, details, error: fetchError } = analyticsMap;
|
||||
if (e.data.id !== undefined && e.data.type === JOB_MAP_NODE_TYPES.TRAINED_MODEL) {
|
||||
// remove training model node related to that job
|
||||
isNotDeletedNodeOrRelated =
|
||||
isNotDeletedNodeOrRelated &&
|
||||
nodeDetails[e.data.id]?.metadata?.analytics_config?.id !== nodeLabel;
|
||||
}
|
||||
|
||||
if (fetchError !== null) {
|
||||
setError(fetchError);
|
||||
}
|
||||
if (destIndexNode !== undefined) {
|
||||
// Filter out destination index node for that job
|
||||
return (
|
||||
isNotDeletedNodeOrRelated &&
|
||||
e.data.id !== destIndexNode &&
|
||||
e.data.target !== destIndexNode &&
|
||||
e.data.source !== destIndexNode
|
||||
);
|
||||
}
|
||||
|
||||
if (nodeElements && nodeElements.length === 0) {
|
||||
notifications.toasts.add(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsMap.emptyResponseMessage', {
|
||||
defaultMessage: 'No related analytics jobs found for {id}.',
|
||||
values: { id: idToUse },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (nodeElements && nodeElements.length > 0) {
|
||||
if (treatAsRoot === false) {
|
||||
setElements(nodeElements);
|
||||
setNodeDetails(details);
|
||||
} else {
|
||||
const uniqueElements = uniqWith([...nodeElements, ...elements], isEqual);
|
||||
setElements(uniqueElements);
|
||||
setNodeDetails({ ...details, ...nodeDetails });
|
||||
}
|
||||
return isNotDeletedNodeOrRelated;
|
||||
});
|
||||
setItemsDeleted(true);
|
||||
setElements(filteredElements);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getDataWrapper();
|
||||
fetchAndSetElementsWrapper({ analyticsId, modelId });
|
||||
}, [analyticsId, modelId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (message !== undefined) {
|
||||
notifications.toasts.add(message);
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
if (error !== undefined) {
|
||||
notifications.toasts.addDanger(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsMap.fetchDataErrorMessage', {
|
||||
|
@ -150,21 +130,63 @@ export const JobMap: FC<Props> = ({ analyticsId, modelId }) => {
|
|||
return (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<div style={{ height: height - parseInt(theme.gutterTypes.gutterLarge, 10) }} ref={ref}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<JobMapTitle analyticsId={analyticsId} modelId={modelId} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<JobMapLegend />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<Cytoscape height={height} elements={elements} width={width} style={cytoscapeDivStyle}>
|
||||
<EuiFlexGroup direction="column" gutterSize="none" justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<JobMapTitle analyticsId={analyticsId} modelId={modelId} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<JobMapLegend />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup gutterSize="xs" component="span">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
data-test-subj={`mlAnalyticsRefreshMapButton${isLoading ? ' loading' : ' loaded'}`}
|
||||
onClick={() => fetchAndSetElementsWrapper({ analyticsId, modelId })}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsList.refreshMapButtonLabel"
|
||||
defaultMessage="Refresh"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="xs"
|
||||
data-test-subj="mlAnalyticsResetGraphButton"
|
||||
// trigger reset on value change
|
||||
onClick={() => setResetCyToggle(!resetCyToggle)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsList.resetMapButtonLabel"
|
||||
defaultMessage="Reset"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<div style={{ height: height - parseInt(theme.gutterTypes.gutterLarge, 10) - 20 }} ref={ref}>
|
||||
<Cytoscape
|
||||
height={height - 20}
|
||||
elements={elements}
|
||||
width={width}
|
||||
style={cytoscapeDivStyle}
|
||||
itemsDeleted={itemsDeleted}
|
||||
resetCy={resetCyToggle}
|
||||
>
|
||||
<Controls
|
||||
details={nodeDetails}
|
||||
getNodeData={getDataWrapper}
|
||||
getNodeData={fetchAndSetElementsWrapper}
|
||||
analyticsId={analyticsId}
|
||||
modelId={modelId}
|
||||
updateElements={updateElements}
|
||||
/>
|
||||
</Cytoscape>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
export const JobMapTitle: FC<{ analyticsId?: string; modelId?: string }> = ({
|
||||
analyticsId,
|
||||
modelId,
|
||||
}) => (
|
||||
<EuiTitle size="xs">
|
||||
<span>
|
||||
{analyticsId ? (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsMap.analyticsIdTitle"
|
||||
defaultMessage="Map for analytics ID {analyticsId}"
|
||||
values={{ analyticsId }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.ml.dataframe.analyticsMap.modelIdTitle"
|
||||
defaultMessage="Map for trained model ID {modelId}"
|
||||
values={{ modelId }}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</EuiTitle>
|
||||
);
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { uniqWith, isEqual } from 'lodash';
|
||||
import cytoscape from 'cytoscape';
|
||||
import { ml } from '../../../services/ml_api_service';
|
||||
import { JOB_MAP_NODE_TYPES } from '../../../../../common/constants/data_frame_analytics';
|
||||
import { AnalyticsMapReturnType } from '../../../../../common/types/data_frame_analytics';
|
||||
|
||||
interface GetDataObjectParameter {
|
||||
analyticsId?: string;
|
||||
id?: string;
|
||||
modelId?: string;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export const useFetchAnalyticsMapData = () => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [elements, setElements] = useState<cytoscape.ElementDefinition[]>([]);
|
||||
const [nodeDetails, setNodeDetails] = useState<Record<string, any>>({});
|
||||
const [error, setError] = useState<any>();
|
||||
const [message, setMessage] = useState<string | undefined>();
|
||||
|
||||
const fetchAndSetElements = async (idToUse: string, treatAsRoot: boolean, type?: string) => {
|
||||
setIsLoading(true);
|
||||
// Pass in treatAsRoot flag - endpoint will take job or index to grab jobs created from it
|
||||
const analyticsMap: AnalyticsMapReturnType = await ml.dataFrameAnalytics.getDataFrameAnalyticsMap(
|
||||
idToUse,
|
||||
treatAsRoot,
|
||||
type
|
||||
);
|
||||
|
||||
const { elements: nodeElements, details, error: fetchError } = analyticsMap;
|
||||
|
||||
if (fetchError !== null) {
|
||||
setIsLoading(false);
|
||||
setError(fetchError);
|
||||
}
|
||||
|
||||
if (nodeElements?.length === 0) {
|
||||
setMessage(
|
||||
i18n.translate('xpack.ml.dataframe.analyticsMap.emptyResponseMessage', {
|
||||
defaultMessage: 'No related analytics jobs found for {id}.',
|
||||
values: { id: idToUse },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (nodeElements?.length > 0) {
|
||||
if (treatAsRoot === false) {
|
||||
setElements(nodeElements);
|
||||
setNodeDetails(details);
|
||||
} else {
|
||||
const uniqueElements = uniqWith([...nodeElements, ...elements], isEqual);
|
||||
setElements(uniqueElements);
|
||||
setNodeDetails({ ...details, ...nodeDetails });
|
||||
}
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const fetchAndSetElementsWrapper = async (params?: GetDataObjectParameter) => {
|
||||
const { analyticsId, id, modelId, type } = params ?? {};
|
||||
const treatAsRoot = id !== undefined;
|
||||
let idToUse: string;
|
||||
|
||||
if (id !== undefined) {
|
||||
idToUse = id;
|
||||
} else if (modelId !== undefined) {
|
||||
idToUse = modelId;
|
||||
} else {
|
||||
idToUse = analyticsId as string;
|
||||
}
|
||||
|
||||
await fetchAndSetElements(
|
||||
idToUse,
|
||||
treatAsRoot,
|
||||
modelId !== undefined && treatAsRoot === false ? JOB_MAP_NODE_TYPES.TRAINED_MODEL : type
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
elements,
|
||||
error,
|
||||
fetchAndSetElementsWrapper,
|
||||
isLoading,
|
||||
message,
|
||||
nodeDetails,
|
||||
setElements,
|
||||
setError,
|
||||
};
|
||||
};
|
|
@ -13,7 +13,10 @@ import {
|
|||
UpdateDataFrameAnalyticsConfig,
|
||||
} from '../../data_frame_analytics/common';
|
||||
import { DeepPartial } from '../../../../common/types/common';
|
||||
import { DeleteDataFrameAnalyticsWithIndexStatus } from '../../../../common/types/data_frame_analytics';
|
||||
import {
|
||||
DeleteDataFrameAnalyticsWithIndexStatus,
|
||||
AnalyticsMapReturnType,
|
||||
} from '../../../../common/types/data_frame_analytics';
|
||||
|
||||
export interface GetDataFrameAnalyticsStatsResponseOk {
|
||||
node_failures?: object;
|
||||
|
@ -83,7 +86,11 @@ export const dataFrameAnalytics = {
|
|||
body,
|
||||
});
|
||||
},
|
||||
getDataFrameAnalyticsMap(id: string, treatAsRoot: boolean, type?: string) {
|
||||
getDataFrameAnalyticsMap(
|
||||
id: string,
|
||||
treatAsRoot: boolean,
|
||||
type?: string
|
||||
): Promise<AnalyticsMapReturnType> {
|
||||
const idString = id !== undefined ? `/${id}` : '';
|
||||
return http({
|
||||
path: `${basePath()}/data_frame/analytics/map${idString}`,
|
||||
|
|
|
@ -13,8 +13,10 @@ import {
|
|||
DataFrameAnalyticsExplorationUrlState,
|
||||
DataFrameAnalyticsUrlState,
|
||||
ExplorationPageUrlState,
|
||||
MlGenericUrlState,
|
||||
MlCommonGlobalState,
|
||||
} from '../../common/types/ml_url_generator';
|
||||
import { createGenericMlUrl } from './common';
|
||||
import { ML_PAGES } from '../../common/constants/ml_url_generator';
|
||||
import { setStateToKbnUrl } from '../../../../../src/plugins/kibana_utils/public';
|
||||
import { getGroupQueryText, getJobQueryText } from '../../common/util/string_utils';
|
||||
|
@ -110,6 +112,16 @@ export function createDataFrameAnalyticsExplorationUrl(
|
|||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates URL to the DataFrameAnalytics creation wizard
|
||||
*/
|
||||
export function createDataFrameAnalyticsCreateJobUrl(
|
||||
appBasePath: string,
|
||||
pageState: MlGenericUrlState['pageState']
|
||||
): string {
|
||||
return createGenericMlUrl(appBasePath, ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB, pageState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates URL to the DataFrameAnalytics Map page
|
||||
*/
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from './anomaly_detection_urls_generator';
|
||||
import {
|
||||
createDataFrameAnalyticsJobManagementUrl,
|
||||
createDataFrameAnalyticsCreateJobUrl,
|
||||
createDataFrameAnalyticsExplorationUrl,
|
||||
createDataFrameAnalyticsMapUrl,
|
||||
} from './data_frame_analytics_urls_generator';
|
||||
|
@ -69,7 +70,8 @@ export class MlUrlGenerator implements UrlGeneratorsDefinition<typeof ML_APP_URL
|
|||
return createSingleMetricViewerUrl(appBasePath, mlUrlGeneratorState.pageState);
|
||||
case ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE:
|
||||
return createDataFrameAnalyticsJobManagementUrl(appBasePath, mlUrlGeneratorState.pageState);
|
||||
// @ts-ignore // TODO: fix type
|
||||
case ML_PAGES.DATA_FRAME_ANALYTICS_CREATE_JOB:
|
||||
return createDataFrameAnalyticsCreateJobUrl(appBasePath, mlUrlGeneratorState.pageState);
|
||||
case ML_PAGES.DATA_FRAME_ANALYTICS_MAP:
|
||||
// @ts-ignore // TODO: fix type
|
||||
return createDataFrameAnalyticsMapUrl(appBasePath, mlUrlGeneratorState.pageState);
|
||||
|
|
|
@ -11,12 +11,16 @@ import {
|
|||
JobMapNodeTypes,
|
||||
} from '../../../common/constants/data_frame_analytics';
|
||||
import { TrainedModelConfigResponse } from '../../../common/types/trained_models';
|
||||
import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer';
|
||||
import { getAnalysisType } from '../../../common/util/analytics_utils';
|
||||
import {
|
||||
AnalyticsMapEdgeElement,
|
||||
AnalyticsMapReturnType,
|
||||
AnalyticsMapNodeElement,
|
||||
DataFrameAnalyticsStats,
|
||||
MapElements,
|
||||
} from '../../../common/types/data_frame_analytics';
|
||||
import { INDEX_META_DATA_CREATED_BY } from '../../../common/constants/file_datavisualizer';
|
||||
import { getAnalysisType } from '../../../common/util/analytics_utils';
|
||||
import {
|
||||
ExtendAnalyticsMapArgs,
|
||||
GetAnalyticsMapArgs,
|
||||
InitialElementsReturnType,
|
||||
|
@ -26,7 +30,6 @@ import {
|
|||
isIndexPatternLinkReturnType,
|
||||
isJobDataLinkReturnType,
|
||||
isTransformLinkReturnType,
|
||||
MapElements,
|
||||
NextLinkReturnType,
|
||||
} from './types';
|
||||
import type { MlClient } from '../../lib/ml_client';
|
||||
|
@ -34,12 +37,22 @@ import type { MlClient } from '../../lib/ml_client';
|
|||
export class AnalyticsManager {
|
||||
private _client: IScopedClusterClient['asInternalUser'];
|
||||
private _mlClient: MlClient;
|
||||
public _inferenceModels: TrainedModelConfigResponse[];
|
||||
private _inferenceModels: TrainedModelConfigResponse[];
|
||||
private _jobStats: DataFrameAnalyticsStats[];
|
||||
|
||||
constructor(mlClient: MlClient, client: IScopedClusterClient['asInternalUser']) {
|
||||
this._client = client;
|
||||
this._mlClient = mlClient;
|
||||
this._inferenceModels = [];
|
||||
this._jobStats = [];
|
||||
}
|
||||
|
||||
public set jobStats(stats) {
|
||||
this._jobStats = stats;
|
||||
}
|
||||
|
||||
public get jobStats() {
|
||||
return this._jobStats;
|
||||
}
|
||||
|
||||
public set inferenceModels(models) {
|
||||
|
@ -55,12 +68,21 @@ export class AnalyticsManager {
|
|||
const models = await this.getAnalyticsModels();
|
||||
this.inferenceModels = models;
|
||||
} catch (error) {
|
||||
// TODO: bubble up this error?
|
||||
// eslint-disable-next-line
|
||||
console.error('Unable to fetch inference models', error);
|
||||
}
|
||||
}
|
||||
|
||||
async setJobStats() {
|
||||
try {
|
||||
const jobStats = await this.getAnalyticsStats();
|
||||
this.jobStats = jobStats;
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line
|
||||
console.error('Unable to fetch job stats', error);
|
||||
}
|
||||
}
|
||||
|
||||
private isDuplicateElement(analyticsId: string, elements: MapElements[]): boolean {
|
||||
let isDuplicate = false;
|
||||
elements.forEach((elem) => {
|
||||
|
@ -89,6 +111,12 @@ export class AnalyticsManager {
|
|||
return models;
|
||||
}
|
||||
|
||||
private async getAnalyticsStats() {
|
||||
const resp = await this._mlClient.getDataFrameAnalyticsStats({ size: 1000 });
|
||||
const stats = resp?.body?.data_frame_analytics;
|
||||
return stats;
|
||||
}
|
||||
|
||||
private async getAnalyticsData(analyticsId?: string) {
|
||||
const options = analyticsId
|
||||
? {
|
||||
|
@ -96,10 +124,20 @@ export class AnalyticsManager {
|
|||
}
|
||||
: undefined;
|
||||
const resp = await this._mlClient.getDataFrameAnalytics(options);
|
||||
const jobData = analyticsId
|
||||
let jobData = analyticsId
|
||||
? resp?.body?.data_frame_analytics[0]
|
||||
: resp?.body?.data_frame_analytics;
|
||||
|
||||
if (analyticsId !== undefined) {
|
||||
const jobStats = this.findJobStats(analyticsId);
|
||||
jobData = { ...jobData, stats: { ...jobStats } };
|
||||
} else {
|
||||
jobData = jobData.map((job: any) => {
|
||||
const jobStats = this.findJobStats(job.id);
|
||||
return { ...job, stats: { ...jobStats } };
|
||||
});
|
||||
}
|
||||
|
||||
return jobData;
|
||||
}
|
||||
|
||||
|
@ -121,10 +159,14 @@ export class AnalyticsManager {
|
|||
|
||||
private findJobModel(analyticsId: string): any {
|
||||
return this.inferenceModels.find(
|
||||
(model: any) => model.metadata?.analytics_config?.id === analyticsId
|
||||
(model) => model.metadata?.analytics_config?.id === analyticsId
|
||||
);
|
||||
}
|
||||
|
||||
private findJobStats(analyticsId: string): DataFrameAnalyticsStats | undefined {
|
||||
return this.jobStats.find((js) => js.id === analyticsId);
|
||||
}
|
||||
|
||||
private async getNextLink({
|
||||
id,
|
||||
type,
|
||||
|
@ -243,31 +285,37 @@ export class AnalyticsManager {
|
|||
details[modelNodeId] = data;
|
||||
// fetch source job data and create elements
|
||||
if (sourceJobId !== undefined) {
|
||||
data = await this.getAnalyticsData(sourceJobId);
|
||||
try {
|
||||
data = await this.getAnalyticsData(sourceJobId);
|
||||
nextLinkId = data?.source?.index[0];
|
||||
nextType = JOB_MAP_NODE_TYPES.INDEX;
|
||||
|
||||
nextLinkId = data?.source?.index[0];
|
||||
nextType = JOB_MAP_NODE_TYPES.INDEX;
|
||||
previousNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`;
|
||||
|
||||
previousNodeId = `${data.id}-${JOB_MAP_NODE_TYPES.ANALYTICS}`;
|
||||
resultElements.push({
|
||||
data: {
|
||||
id: previousNodeId,
|
||||
label: data.id,
|
||||
type: JOB_MAP_NODE_TYPES.ANALYTICS,
|
||||
analysisType: getAnalysisType(data?.analysis),
|
||||
},
|
||||
});
|
||||
// Create edge between job and model
|
||||
modelElements.push({
|
||||
data: {
|
||||
id: `${previousNodeId}~${modelNodeId}`,
|
||||
source: previousNodeId,
|
||||
target: modelNodeId,
|
||||
},
|
||||
});
|
||||
|
||||
resultElements.push({
|
||||
data: {
|
||||
id: previousNodeId,
|
||||
label: data.id,
|
||||
type: JOB_MAP_NODE_TYPES.ANALYTICS,
|
||||
analysisType: getAnalysisType(data?.analysis),
|
||||
},
|
||||
});
|
||||
// Create edge between job and model
|
||||
modelElements.push({
|
||||
data: {
|
||||
id: `${previousNodeId}~${modelNodeId}`,
|
||||
source: previousNodeId,
|
||||
target: modelNodeId,
|
||||
},
|
||||
});
|
||||
|
||||
details[previousNodeId] = data;
|
||||
details[previousNodeId] = data;
|
||||
} catch (error) {
|
||||
// fail silently if job doesn't exist
|
||||
if (error.statusCode !== 404) {
|
||||
throw error.body ?? error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { data, details, resultElements, modelElements, nextLinkId, nextType, previousNodeId };
|
||||
|
@ -325,7 +373,7 @@ export class AnalyticsManager {
|
|||
const indexPatternElements: MapElements[] = [];
|
||||
|
||||
try {
|
||||
await this.setInferenceModels();
|
||||
await Promise.all([this.setInferenceModels(), this.setJobStats()]);
|
||||
// Create first node for incoming analyticsId or modelId
|
||||
let initialData: InitialElementsReturnType = {} as InitialElementsReturnType;
|
||||
if (analyticsId !== undefined) {
|
||||
|
@ -532,7 +580,7 @@ export class AnalyticsManager {
|
|||
}: ExtendAnalyticsMapArgs): Promise<AnalyticsMapReturnType> {
|
||||
const result: AnalyticsMapReturnType = { elements: [], details: {}, error: null };
|
||||
try {
|
||||
await this.setInferenceModels();
|
||||
await Promise.all([this.setInferenceModels(), this.setJobStats()]);
|
||||
const jobs = await this.getAnalyticsData();
|
||||
let rootIndex;
|
||||
let rootIndexNodeId;
|
||||
|
|
|
@ -5,6 +5,17 @@
|
|||
*/
|
||||
|
||||
import { JobMapNodeTypes } from '../../../common/constants/data_frame_analytics';
|
||||
import {
|
||||
MapElements,
|
||||
AnalyticsMapNodeElement,
|
||||
AnalyticsMapEdgeElement,
|
||||
} from '../../../common/types/data_frame_analytics';
|
||||
export {
|
||||
MapElements,
|
||||
AnalyticsMapReturnType,
|
||||
AnalyticsMapNodeElement,
|
||||
AnalyticsMapEdgeElement,
|
||||
} from '../../../common/types/data_frame_analytics';
|
||||
|
||||
interface AnalyticsMapArg {
|
||||
analyticsId: string;
|
||||
|
@ -46,12 +57,6 @@ export type NextLinkReturnType =
|
|||
| JobDataLinkReturnType
|
||||
| TransformLinkReturnType
|
||||
| undefined;
|
||||
export type MapElements = AnalyticsMapNodeElement | AnalyticsMapEdgeElement;
|
||||
export interface AnalyticsMapReturnType {
|
||||
elements: MapElements[];
|
||||
details: Record<string, any>; // transform, job, or index details
|
||||
error: null | any;
|
||||
}
|
||||
|
||||
interface BasicInitialElementsReturnType {
|
||||
data: any;
|
||||
|
@ -70,29 +75,18 @@ interface CompleteInitialElementsReturnType extends BasicInitialElementsReturnTy
|
|||
nextType: JobMapNodeTypes;
|
||||
previousNodeId: string;
|
||||
}
|
||||
export interface AnalyticsMapNodeElement {
|
||||
data: {
|
||||
id: string;
|
||||
label: string;
|
||||
type: string;
|
||||
analysisType?: string;
|
||||
};
|
||||
}
|
||||
export interface AnalyticsMapEdgeElement {
|
||||
data: {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const isCompleteInitialReturnType = (arg: any): arg is CompleteInitialElementsReturnType => {
|
||||
if (typeof arg !== 'object' || arg === null) return false;
|
||||
const keys = Object.keys(arg);
|
||||
return (
|
||||
keys.length > 0 &&
|
||||
keys.includes('nextLinkId') &&
|
||||
arg.nextLinkId !== undefined &&
|
||||
keys.includes('nextType') &&
|
||||
keys.includes('previousNodeId')
|
||||
arg.nextType !== undefined &&
|
||||
keys.includes('previousNodeId') &&
|
||||
arg.previousNodeId !== undefined
|
||||
);
|
||||
};
|
||||
export const isAnalyticsMapNodeElement = (arg: any): arg is AnalyticsMapNodeElement => {
|
||||
|
|
|
@ -11,7 +11,8 @@ import { Annotation } from '../../../../plugins/ml/common/types/annotations';
|
|||
import { DataFrameAnalyticsConfig } from '../../../../plugins/ml/public/application/data_frame_analytics/common';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { DATAFEED_STATE, JOB_STATE } from '../../../../plugins/ml/common/constants/states';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/data_frame_task_state';
|
||||
import { DataFrameTaskStateType } from '../../../../plugins/ml/common/types/data_frame_analytics';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/common/constants/data_frame_analytics';
|
||||
import { Datafeed, Job } from '../../../../plugins/ml/common/types/anomaly_detection_jobs';
|
||||
import { JobType } from '../../../../plugins/ml/common/types/saved_objects';
|
||||
export type MlApi = ProvidedType<typeof MachineLearningAPIProvider>;
|
||||
|
@ -245,7 +246,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
|
|||
return analyticsStats;
|
||||
},
|
||||
|
||||
async getAnalyticsState(analyticsId: string): Promise<DATA_FRAME_TASK_STATE> {
|
||||
async getAnalyticsState(analyticsId: string): Promise<DataFrameTaskStateType> {
|
||||
log.debug(`Fetching analytics state for job ${analyticsId}`);
|
||||
const analyticsStats = await this.getDFAJobStats(analyticsId);
|
||||
|
||||
|
@ -254,7 +255,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
|
|||
`Expected dataframe analytics stats to have exactly one object (got '${analyticsStats.data_frame_analytics.length}')`
|
||||
);
|
||||
|
||||
const state: DATA_FRAME_TASK_STATE = analyticsStats.data_frame_analytics[0].state;
|
||||
const state: DataFrameTaskStateType = analyticsStats.data_frame_analytics[0].state;
|
||||
|
||||
return state;
|
||||
},
|
||||
|
@ -291,7 +292,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) {
|
|||
|
||||
async waitForAnalyticsState(
|
||||
analyticsId: string,
|
||||
expectedAnalyticsState: DATA_FRAME_TASK_STATE
|
||||
expectedAnalyticsState: DataFrameTaskStateType
|
||||
) {
|
||||
await retry.waitForWithTimeout(
|
||||
`analytics state to be ${expectedAnalyticsState}`,
|
||||
|
|
|
@ -9,7 +9,7 @@ import expect from '@kbn/expect';
|
|||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { MlApi } from './api';
|
||||
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/data_frame_task_state';
|
||||
import { DATA_FRAME_TASK_STATE } from '../../../../plugins/ml/common/constants/data_frame_analytics';
|
||||
|
||||
export function MachineLearningDataFrameAnalyticsProvider(
|
||||
{ getService }: FtrProviderContext,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue